diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index c59f2cf6c7d1f739351734f9e300c144ced9e9c9..b3225a1f2aa3536b48e7f68f44220d42958af6eb 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -9,11 +9,6 @@
             "problemMatcher": [
                 "$tsc"
             ]
-        },
-        {
-            "type": "npm",
-            "script": "app-render-test",
-            "problemMatcher": []
         }
     ]
 }
\ No newline at end of file
diff --git a/README.md b/README.md
index fc10efd04fb3ebad34988a10a59f19f0e554a35d..a03c9462eb3ba0a884e85a3f5facb8de1830d000 100644
--- a/README.md
+++ b/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.
diff --git a/data/rcsb-graphql/codegen.js b/data/rcsb-graphql/codegen.js
index 8982097ff6726ed4918d5547ac4b0cd40d9b34e8..1abc6b4f93e93c94235395dd748bd396c945a641 100644
--- a/data/rcsb-graphql/codegen.js
+++ b/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(
diff --git a/data/rcsb-graphql/loader.js b/data/rcsb-graphql/loader.js
new file mode 100644
index 0000000000000000000000000000000000000000..73e76e1c421b88367f334b95621e53710fe5d153
--- /dev/null
+++ b/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)
+        }
+    ];
+};
\ No newline at end of file
diff --git a/docs/state/example-state.json b/docs/state/example-state.json
new file mode 100644
index 0000000000000000000000000000000000000000..91a20da2d5552f2cc656518ca05939c5bdbf7179
--- /dev/null
+++ b/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
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/docs/state/readme.md b/docs/state/readme.md
new file mode 100644
index 0000000000000000000000000000000000000000..22ae1ccc62d1f76e148bfbf672ac0b0ca2365d23
--- /dev/null
+++ b/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
+}
+```
\ No newline at end of file
diff --git a/docs/state/transforms.md b/docs/state/transforms.md
new file mode 100644
index 0000000000000000000000000000000000000000..473030fade96825b498d9dc66bb053382da39c1d
--- /dev/null
+++ b/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": []
+}
+```
+----------------------------
diff --git a/package-lock.json b/package-lock.json
index b33e22352472bcd4abcb488c386ffd92af540934..8f86e5adeac89d096105360c76b4de9fcdd01ed1 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index 918f5f324f9b8f990b1cc8a9f129bafb46b855d2..430c3c900dc84aab9210303940f250a3f5431885 100644
--- a/package.json
+++ b/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"
   }
 }
diff --git a/src/apps/basic-wrapper/coloring.ts b/src/apps/basic-wrapper/coloring.ts
new file mode 100644
index 0000000000000000000000000000000000000000..54c89117c7f78c5209364fab76dcd1d35f3f7599
--- /dev/null
+++ b/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'
+    }
+})
\ No newline at end of file
diff --git a/src/apps/basic-wrapper/index.html b/src/apps/basic-wrapper/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..efa0b8c9f43fe1a4085acd2509b11096e997234c
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..aaa87f3d9a2788661fd03c5371bb4af8124c4634
--- /dev/null
+++ b/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();
\ No newline at end of file
diff --git a/src/apps/chem-comp-bond/create-table.ts b/src/apps/chem-comp-bond/create-table.ts
index c30bc04b270c127e4d37eeb58e4e83b80a34d55c..28dc5000fc972af9ced1ef12f1205c068006eaf4 100644
--- a/src/apps/chem-comp-bond/create-table.ts
+++ b/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)) {
diff --git a/src/apps/schema-generator/util/cif-dic.ts b/src/apps/schema-generator/util/cif-dic.ts
index 8376fcb3d35e776ca12abb4a4940894ab614d0eb..e800ba3d350fc92f6610d57f3d41ad7079780bf8 100644
--- a/src/apps/schema-generator/util/cif-dic.ts
+++ b/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',
diff --git a/src/apps/state-docs/index.ts b/src/apps/state-docs/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1cd02da05e62fae0441d5f98c27f2bdf887a8bab
--- /dev/null
+++ b/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));
\ No newline at end of file
diff --git a/src/apps/state-docs/pd-to-md.ts b/src/apps/state-docs/pd-to-md.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b04c25e3a26a0fdcbed5bc7a958b00a0174cca37
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/src/apps/structure-info/model.ts b/src/apps/structure-info/model.ts
index c01698f91a40bda96ebad2fd4955b18e9a1546a7..58c76c69f32b3202f852b75bf5d1a54126eb3de4 100644
--- a/src/apps/structure-info/model.ts
+++ b/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,
diff --git a/src/apps/structure-info/volume.ts b/src/apps/structure-info/volume.ts
index 2faaf7086b40f47dd7e77753997805e0d8cbbb81..4b3e54895e3e92f3b46c9abd8295652a16381ff7 100644
--- a/src/apps/structure-info/volume.ts
+++ b/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.
diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cdf0026b39a4201f802fe01f18bc736c2ca0c71e
--- /dev/null
+++ b/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
+    });
+}
\ No newline at end of file
diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html
index 6f2809d5e30883dd8872f931c3428f0731b3a19e..4cb1036165d7c26dea7b8cfa1e85830641ac547e 100644
--- a/src/apps/viewer/index.html
+++ b/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>
\ No newline at end of file
diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts
index 6e103ee755535db5599d3c284aaa975a6f45990a..d67a0e9396a68933718ec84730cd55f7f22bfaab 100644
--- a/src/apps/viewer/index.ts
+++ b/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')!);
\ No newline at end of file
+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();
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/annotation.ts b/src/examples/proteopedia-wrapper/annotation.ts
new file mode 100644
index 0000000000000000000000000000000000000000..66bf2ea7c0ceaec0d6eabfd6268bc4364df71713
--- /dev/null
+++ b/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;
+    }
+});
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/changelog.md b/src/examples/proteopedia-wrapper/changelog.md
new file mode 100644
index 0000000000000000000000000000000000000000..041ecacd3454949a8be9e03ad2a6284c5b9f8381
--- /dev/null
+++ b/src/examples/proteopedia-wrapper/changelog.md
@@ -0,0 +1,7 @@
+== v2.0 ==
+
+* Changed how state saving works.
+
+== v1.0 ==
+
+* Initial version.
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..427fb057e0b423573a13bb1ea934a977e75a8845
--- /dev/null
+++ b/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 }
+}
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/index.html b/src/examples/proteopedia-wrapper/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..009eb5bf8193a534a6a2c21e342e1c47cfb29e6d
--- /dev/null
+++ b/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>
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..91677335b5ee870e06931e007c0ca3b7857db417
--- /dev/null
+++ b/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;
\ No newline at end of file
diff --git a/src/examples/proteopedia-wrapper/ui/controls.tsx b/src/examples/proteopedia-wrapper/ui/controls.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d2a79e61b59b5df991b610381e77c7d9e4fe8ecc
--- /dev/null
+++ b/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>;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index 814503272e6f6c3da610e6a438bb32c977a343a6..e4491ed0f88c6e563d3637a4bcdeb38108b25c2a 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/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()
             }
         }
 
diff --git a/src/mol-canvas3d/controls/trackball.ts b/src/mol-canvas3d/controls/trackball.ts
index 40f34459a4a7d1a638c1de5b1548aa529b8a7dd3..329cefae50aaa603ae638d23dd96e3bb939eb341 100644
--- a/src/mol-canvas3d/controls/trackball.ts
+++ b/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,
diff --git a/src/mol-canvas3d/helper/bounding-sphere-helper.ts b/src/mol-canvas3d/helper/bounding-sphere-helper.ts
index 6fc1a26c2c333b98b693375f70ee656e2355dca4..c3272d840f4a26240a0ef4ae42cc8ba8821ddd75 100644
--- a/src/mol-canvas3d/helper/bounding-sphere-helper.ts
+++ b/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 })
 }
\ No newline at end of file
diff --git a/src/mol-canvas3d/helper/interaction-events.ts b/src/mol-canvas3d/helper/interaction-events.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4267ecf0a02bea0050eb6b4a1fe1644033abdeb4
--- /dev/null
+++ b/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));
+    }
+}
\ No newline at end of file
diff --git a/src/mol-data/int/_spec/ordered-set.spec.ts b/src/mol-data/int/_spec/ordered-set.spec.ts
index 401bff07e298b66caaee62914e110a480c66b10f..5f717b2dd40dc985026c67eb36ff977dffbb4830 100644
--- a/src/mol-data/int/_spec/ordered-set.spec.ts
+++ b/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])), []);
diff --git a/src/mol-data/int/impl/ordered-set.ts b/src/mol-data/int/impl/ordered-set.ts
index 33794046c652e26bba6469695c510d928ad46977..832a0c28147326c66ce82c4bc80e971489d4a22e 100644
--- a/src/mol-data/int/impl/ordered-set.ts
+++ b/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);
 }
diff --git a/src/mol-data/int/impl/sorted-array.ts b/src/mol-data/int/impl/sorted-array.ts
index 6fff1aa007c47f26cc99d19e8ed790e0e57c4398..0e6336af2f8fdbf2e56713dcce86481cdfd0ab61 100644
--- a/src/mol-data/int/impl/sorted-array.ts
+++ b/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]);
 }
 
diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts
index 2729048b8d9230efa2deb8bd9e453152132b228f..eabbbc2454694f7f9e6696f68401f1d23f3ad782 100644
--- a/src/mol-geo/geometry/base.ts
+++ b/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
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/geometry/color-data.ts b/src/mol-geo/geometry/color-data.ts
index d244746fd63c3be891d0fcdcae38c66d50a848ea..19acb4e3396212cbafad013d967fd5ac5eeb1a2c 100644
--- a/src/mol-geo/geometry/color-data.ts
+++ b/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()
diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts
index 47d89aad9a8953282ead03c08ecdae361197c201..ea2c9694c19dd979d2245baba0cde0a8050eb37d 100644
--- a/src/mol-geo/geometry/direct-volume/direct-volume.ts
+++ b/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)
diff --git a/src/mol-geo/geometry/lines/lines-builder.ts b/src/mol-geo/geometry/lines/lines-builder.ts
index db6ae4b45f0e5162cf755fff1f3aad23522c3830..70d994dbbed725b91fb0a01836df7b5759ce7ec1 100644
--- a/src/mol-geo/geometry/lines/lines-builder.ts
+++ b/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
diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts
index ef5d980f8539daaa64af48d9d568b97008932da5..a2795a9c8646390fd2fc0455ba9a779429ac9870 100644
--- a/src/mol-geo/geometry/lines/lines.ts
+++ b/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)
diff --git a/src/mol-geo/geometry/marker-data.ts b/src/mol-geo/geometry/marker-data.ts
index 3fd6dc64dc224dbab0320bae244de3d231deeb1c..2c49bd1b233b05d234831506791289b26de0ecd6 100644
--- a/src/mol-geo/geometry/marker-data.ts
+++ b/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))
diff --git a/src/mol-geo/geometry/mesh/builder/sheet.ts b/src/mol-geo/geometry/mesh/builder/sheet.ts
index ec0fcd5e0b8740ab0c98b7c5306b215c7b6e9973..765b96fcad32f6a16ce22154ad169f2b405a78e5 100644
--- a/src/mol-geo/geometry/mesh/builder/sheet.ts
+++ b/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) {
diff --git a/src/mol-geo/geometry/mesh/builder/tube.ts b/src/mol-geo/geometry/mesh/builder/tube.ts
index d60dd528dae400f318347d87753903715b48f367..693bc69d8cd9d0a90ed6f3160f1dab7db7170a56 100644
--- a/src/mol-geo/geometry/mesh/builder/tube.ts
+++ b/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
diff --git a/src/mol-geo/geometry/mesh/mesh-builder.ts b/src/mol-geo/geometry/mesh/mesh-builder.ts
index 742f41e719a5114e7b0862e61f3816086b79d89e..2bcaf085159f4597b6ee0c0d915b754c6aeac46c 100644
--- a/src/mol-geo/geometry/mesh/mesh-builder.ts
+++ b/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
diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts
index 272cdfd03a458a354ba9048c3c3e824c0da55a42..fd594d952c6ad0aa3f886828537e0f833651b53c 100644
--- a/src/mol-geo/geometry/mesh/mesh.ts
+++ b/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),
diff --git a/src/mol-geo/geometry/overpaint-data.ts b/src/mol-geo/geometry/overpaint-data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..875bef81b528dd5989f394dbfd4e0cfdf53870bb
--- /dev/null
+++ b/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),
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts
index ecfe127bf764840c2f3521cfd07db4f13dd3f303..7cda2e36ef2d8241561cdbcb1a341cfbf99d1446 100644
--- a/src/mol-geo/geometry/points/points.ts
+++ b/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),
diff --git a/src/mol-geo/geometry/size-data.ts b/src/mol-geo/geometry/size-data.ts
index fa96fc266b0c11a2cc3fb524903775c14050108d..fb4b94b851027ba336703e18d8502f2b34c6338b 100644
--- a/src/mol-geo/geometry/size-data.ts
+++ b/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()
diff --git a/src/mol-geo/geometry/spheres/spheres-builder.ts b/src/mol-geo/geometry/spheres/spheres-builder.ts
index b4fb62c2ade317ad82e988e6e5db2cf4a36580c5..9050e723a3bbb0f0d8ce415ebbf19b5078d651a7 100644
--- a/src/mol-geo/geometry/spheres/spheres-builder.ts
+++ b/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),
                 }
diff --git a/src/mol-geo/geometry/spheres/spheres.ts b/src/mol-geo/geometry/spheres/spheres.ts
index 41bd89bfc7f3f604e0fb5ac0eb4f8c5cd4381cc2..eb22f55a918043f512f28824d857b3fb61d00db1 100644
--- a/src/mol-geo/geometry/spheres/spheres.ts
+++ b/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),
diff --git a/src/mol-geo/geometry/text/text-builder.ts b/src/mol-geo/geometry/text/text-builder.ts
index 200beecffddbb876fd72e60ffea978968275e01e..ec3e077d56b8bee5df8dd891309bb8e19e7c3818 100644
--- a/src/mol-geo/geometry/text/text-builder.ts
+++ b/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),
diff --git a/src/mol-geo/geometry/text/text.ts b/src/mol-geo/geometry/text/text.ts
index 6b2534891a0c565e25411da64ce68bf6e93a56d6..4238f2432cc9b24f9cb21e2d3dbf2841c70179af 100644
--- a/src/mol-geo/geometry/text/text.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/geometry/transform-data.ts b/src/mol-geo/geometry/transform-data.ts
index 03e107a88ec684ae447cd1ec06d18a6d57ba26ef..de4b2b797cc8bdb94d2b02a43459cf799854858d 100644
--- a/src/mol-geo/geometry/transform-data.ts
+++ b/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)
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/box.ts b/src/mol-geo/primitive/box.ts
index d3fa7f74dd3bf158dd0756d2101aca4921e66931..5ce466fe7188a9543f5f033e875b0b5b3679e409 100644
--- a/src/mol-geo/primitive/box.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/cage.ts b/src/mol-geo/primitive/cage.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6c235c61f28ff2fecfd317bad4568f99c3233800
--- /dev/null
+++ b/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 }
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/dodecahedron.ts b/src/mol-geo/primitive/dodecahedron.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d54ada78f4b1a5008525b71f91378dcd14304fb1
--- /dev/null
+++ b/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
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/icosahedron.ts b/src/mol-geo/primitive/icosahedron.ts
index 096945994adb3b20a1bc5680814117cbecc7a694..4be044a7dd0f8f3b80f8168ab02ea3437844b77f 100644
--- a/src/mol-geo/primitive/icosahedron.ts
+++ b/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 }
\ No newline at end of file
+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
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/octahedron.ts b/src/mol-geo/primitive/octahedron.ts
index cb23691c9cb84b24b83677026009649e1a462453..989526767fff67b6824c9ebb412f32742434904e 100644
--- a/src/mol-geo/primitive/octahedron.ts
+++ b/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 }
\ No newline at end of file
+const octahedronCage = createCage(octahedronVertices, octahedronEdges)
+export function OctahedronCage(): Cage {
+    return octahedronCage
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/plane.ts b/src/mol-geo/primitive/plane.ts
index 2f30c427d4fd4ccf7794b135ae2169aa3793e917..f0886e6270d1a6b8b29a95a87c66bb5f1db50aa4 100644
--- a/src/mol-geo/primitive/plane.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/polygon.ts b/src/mol-geo/primitive/polygon.ts
index b7db9972eb6511dc1cab248e47c658594985bb8d..d26aaed98f05b0abc48cdb0817962b522422f77b 100644
--- a/src/mol-geo/primitive/polygon.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/prism.ts b/src/mol-geo/primitive/prism.ts
index 02bbee0b26176c27c36d6502cd83523fba8b3ace..d848f2d6168121b0d77f0389b43602372f5a4eb4 100644
--- a/src/mol-geo/primitive/prism.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/pyramid.ts b/src/mol-geo/primitive/pyramid.ts
index c00b84320cd714f967c8a89519fef216f5b95487..242dcbae5a131f5cf61acd06a3b0bf2e18de81ed 100644
--- a/src/mol-geo/primitive/pyramid.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-geo/primitive/spiked-ball.ts b/src/mol-geo/primitive/spiked-ball.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dd2534f7327fe5ab576c16f4712a43c0edf71a93
--- /dev/null
+++ b/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)
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/tetrahedron.ts b/src/mol-geo/primitive/tetrahedron.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fd89bf8e118636b3b7efa934461e57eca1549dd9
--- /dev/null
+++ b/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
+}
\ No newline at end of file
diff --git a/src/mol-geo/primitive/wedge.ts b/src/mol-geo/primitive/wedge.ts
index cfd912b53533d0b1283af25f257d3d72c88a8030..a39125f4fd7b37dc9138abddf1e70fd640801835 100644
--- a/src/mol-geo/primitive/wedge.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts
index 10b09294ee8f53f87e578860883b2802d1a9871d..314d75f0bd3dc31c67d860fd296c56e969a8c4b1 100644
--- a/src/mol-gl/_spec/renderer.spec.ts
+++ b/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);
diff --git a/src/mol-gl/renderable.ts b/src/mol-gl/renderable.ts
index f61ca5f5bdadf2ff118757df8cb22bdca3ffedfa..34f0eee4159c8750f94c7eabc9669119e7b4a6b8 100644
--- a/src/mol-gl/renderable.ts
+++ b/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)
             }
diff --git a/src/mol-gl/renderable/direct-volume.ts b/src/mol-gl/renderable/direct-volume.ts
index 6010ff6fe79e4e51cdd2fa0bcf3a44038ddf5916..eb94c44996943bdd529c67c93898f25375b9009c 100644
--- a/src/mol-gl/renderable/direct-volume.ts
+++ b/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'),
 
diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts
index bfcc32ca8a5003a773e0957379e07fe467555e48..28e36306fe14d52b691e0da71cbc5781e3702da1 100644
--- a/src/mol-gl/renderable/schema.ts
+++ b/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'),
diff --git a/src/mol-gl/renderable/text.ts b/src/mol-gl/renderable/text.ts
index 7e633f37d8ae6e8b832d6e87a2068cd1dee25cf3..6f10a8fad2fc94589cfe215d39f0bbd8466b5c0e 100644
--- a/src/mol-gl/renderable/text.ts
+++ b/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),
diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts
index 2d70433f1cd6c530806feae290fb6b96fd5a41bb..a8d4d0c8b0e74f52f05b3d70395380cde3f75040 100644
--- a/src/mol-gl/renderable/util.ts
+++ b/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) {
diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts
index d744286b1f619c027f43c5ef2dbeb3260ed98a81..e8e079ceb5d01b3aeb5e0a07442bc510e0d481a4 100644
--- a/src/mol-gl/scene.ts
+++ b/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) => {
diff --git a/src/mol-gl/shader/chunks/assign-color-varying.glsl b/src/mol-gl/shader/chunks/assign-color-varying.glsl
index e2e7e150af8b051086c3a1827e717e7a074588b4..7175df041fa3cf59c66964a3a0598d49ac20c05f 100644
--- a/src/mol-gl/shader/chunks/assign-color-varying.glsl
+++ b/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
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl b/src/mol-gl/shader/chunks/assign-material-color.glsl
index 81758eabcd3b6630f7e7cd1cf3f2f819f419695e..d79b00d7da917237b2e5c1b657ccf1683adbd14b 100644
--- a/src/mol-gl/shader/chunks/assign-material-color.glsl
+++ b/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
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/color-frag-params.glsl b/src/mol-gl/shader/chunks/color-frag-params.glsl
index 85052a17025d578608312aeae2d04d49519c1eee..871e2dee3e9551de259c1273ac869154228df38e 100644
--- a/src/mol-gl/shader/chunks/color-frag-params.glsl
+++ b/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
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/color-vert-params.glsl b/src/mol-gl/shader/chunks/color-vert-params.glsl
index d2219d9b6019165b6bafbfde21226b614244c1ba..b3156a22ee6f0cabb1cf1505e09b82570ce865da 100644
--- a/src/mol-gl/shader/chunks/color-vert-params.glsl
+++ b/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
\ No newline at end of file
diff --git a/src/mol-gl/shader/text.vert b/src/mol-gl/shader/text.vert
index 54266ddc7ac0eb7207da4542dab9f96fa6b3a26a..4bed4c542e26fa3ea27d83f08a5db597e87c5e6d 100644
--- a/src/mol-gl/shader/text.vert
+++ b/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 {
diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts
index 7738c7cc3685a774c9c74254cf59fc4d214c9cde..fad8f866e25789f402f9a8ef880d0d6fbeb0441a 100644
--- a/src/mol-gl/webgl/render-item.ts
+++ b/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
         },
diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts
index b08790f3321dd154deff6d8eeb6eb68297fcb8f9..c5128cc4ad2ed9eb3faee3ba3e2bf887ba858adc 100644
--- a/src/mol-gl/webgl/texture.ts
+++ b/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;
diff --git a/src/mol-io/common/file-handle.ts b/src/mol-io/common/file-handle.ts
index b3ae0fd64d1adb4f74c4f93482f2af7a75f8469a..90c6deebfa9466b9c1097d511f67478e6370db2a 100644
--- a/src/mol-io/common/file-handle.ts
+++ b/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) {
+
                 }
             }
         }
diff --git a/src/mol-io/common/simple-buffer.ts b/src/mol-io/common/simple-buffer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9f44ee1dcff10a575d9a19aacf949573ef7c3294
--- /dev/null
+++ b/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);
+    }
+}
\ No newline at end of file
diff --git a/src/mol-io/common/typed-array.ts b/src/mol-io/common/typed-array.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f07e982c367e059bd1c0a6cbedcb6d6dd2c7c7a8
--- /dev/null
+++ b/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;
+}
diff --git a/src/mol-io/reader/_spec/ccp4.spec.ts b/src/mol-io/reader/_spec/ccp4.spec.ts
index fc57da2ad58e4c009ed85cc338dbf22b557c2e81..1fe2202d9d01aa5f19a3138bdac800ec8adf4058 100644
--- a/src/mol-io/reader/_spec/ccp4.spec.ts
+++ b/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)
diff --git a/src/mol-io/reader/_spec/cif.spec.ts b/src/mol-io/reader/_spec/cif.spec.ts
index a2fb03ed952f69e1c9c93878a7b5904d174f3ddd..43e5cd4c5139e3da21b9d7de00d57cae59bae5c0 100644
--- a/src/mol-io/reader/_spec/cif.spec.ts
+++ b/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'], {
diff --git a/src/mol-io/reader/_spec/csv.spec.ts b/src/mol-io/reader/_spec/csv.spec.ts
index 0802bc7491fedc90c702ef6bc24d7fa653746895..8977b5909aa73d7714760f377a3d912048a20db5 100644
--- a/src/mol-io/reader/_spec/csv.spec.ts
+++ b/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;
 
diff --git a/src/mol-io/reader/_spec/gro.spec.ts b/src/mol-io/reader/_spec/gro.spec.ts
index 055e4e61b227a58a86c5a1b4b6317c996558ec98..16a120a387eaeb712c8cb37ca3a8a17df63419d3 100644
--- a/src/mol-io/reader/_spec/gro.spec.ts
+++ b/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)
diff --git a/src/mol-io/reader/ccp4/parser.ts b/src/mol-io/reader/ccp4/parser.ts
index 30e7c9e9db807ed6210956cdd1f2599e7cdbb976..832135f4a8ba2a867b5f8d3ec23d603dc20da6be 100644
--- a/src/mol-io/reader/ccp4/parser.ts
+++ b/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;
\ No newline at end of file
+export function parse(buffer: Uint8Array) {
+    return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length)
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/ccp4/schema.ts b/src/mol-io/reader/ccp4/schema.ts
index 0ef828cf3b26e05ea79d7ce954f3f610770fa9cc..f896f81d7df462218a164bb293308f313dc74180 100644
--- a/src/mol-io/reader/ccp4/schema.ts
+++ b/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
 }
\ No newline at end of file
diff --git a/src/mol-io/reader/cif/binary/parser.ts b/src/mol-io/reader/cif/binary/parser.ts
index 8a5f0ea1a8133aed0254577b2791d75952042183..4cf3ae57f882844f856d28612a943fd7a3515179 100644
--- a/src/mol-io/reader/cif/binary/parser.ts
+++ b/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'
 
diff --git a/src/mol-io/reader/cif/data-model.ts b/src/mol-io/reader/cif/data-model.ts
index fe044426017e052ae96a2e9d420f8c13d52c53c1..2800437dc930bd57af8dafe14913f9d5e15fc105 100644
--- a/src/mol-io/reader/cif/data-model.ts
+++ b/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;
diff --git a/src/mol-io/reader/cif/schema/bird.ts b/src/mol-io/reader/cif/schema/bird.ts
index b6a8950046a8b53a695a28e6ad4856bdb8c9f49d..d8687247fa51c82a8d4ba95f85db40a5b92f7ca8 100644
--- a/src/mol-io/reader/cif/schema/bird.ts
+++ b/src/mol-io/reader/cif/schema/bird.ts
@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-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)
  */
diff --git a/src/mol-io/reader/cif/schema/ccd.ts b/src/mol-io/reader/cif/schema/ccd.ts
index 7086e69b7c6ee5aaa4f7714ff7bb7f581cf34acb..d053c6e1160b3034cd2c5328ccbea1fec2a2f27c 100644
--- a/src/mol-io/reader/cif/schema/ccd.ts
+++ b/src/mol-io/reader/cif/schema/ccd.ts
@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-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)
  */
diff --git a/src/mol-io/reader/cif/schema/mmcif.ts b/src/mol-io/reader/cif/schema/mmcif.ts
index 94ef98f339e9389ac4320ee33d1077dc5fa68ad3..4da0f48ed23c3b4711710280254c10cf11117726 100644
--- a/src/mol-io/reader/cif/schema/mmcif.ts
+++ b/src/mol-io/reader/cif/schema/mmcif.ts
@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-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.
          */
diff --git a/src/mol-io/reader/cif/text/field.ts b/src/mol-io/reader/cif/text/field.ts
deleted file mode 100644
index f4e08a090352acb330abfba07c340e820cbd8d9e..0000000000000000000000000000000000000000
--- a/src/mol-io/reader/cif/text/field.ts
+++ /dev/null
@@ -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)
-    }
-}
\ No newline at end of file
diff --git a/src/mol-io/reader/cif/text/parser.ts b/src/mol-io/reader/cif/text/parser.ts
index 3ee75e270731600d06f6ab72b39d9359c1227f06..920546393c25b5aa585b34d8740d872222e63670 100644
--- a/src/mol-io/reader/cif/text/parser.ts
+++ b/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);
diff --git a/src/mol-io/reader/common/text/tokenizer.ts b/src/mol-io/reader/common/text/tokenizer.ts
index 60f14c1b135bfca7d215f3a3d8939997b4ec2ae5..fce7c9037cc7c9eb546edae1a12fb4d838b06efa 100644
--- a/src/mol-io/reader/common/text/tokenizer.ts
+++ b/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
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/csv/field.ts b/src/mol-io/reader/csv/field.ts
index fdc4c5135d4037d72dbd06385accae9bd805bbfa..48d1f1072aa1a54cc9dd77e94318ecfce6ce14e5 100644
--- a/src/mol-io/reader/csv/field.ts
+++ b/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
\ No newline at end of file
+export default CifField.ofTokens
\ No newline at end of file
diff --git a/src/mol-io/reader/csv/parser.ts b/src/mol-io/reader/csv/parser.ts
index d5bc68535344ff6c9d7aed63445b26e19a9220c7..4207202703338f1300bce9ebc0142c8ac6b39294 100644
--- a/src/mol-io/reader/csv/parser.ts
+++ b/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)
diff --git a/src/mol-io/reader/dsn6/parser.ts b/src/mol-io/reader/dsn6/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..131de466842f8a912655b9086d2ecbc219cba0c9
--- /dev/null
+++ b/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)
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/dsn6/schema.ts b/src/mol-io/reader/dsn6/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6bd612093ebb7a1454120f9a04e1b7917c7f8553
--- /dev/null
+++ b/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
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/gro/parser.ts b/src/mol-io/reader/gro/parser.ts
index 6183a9a5fee6e887e889b4bd6efd4cec5b3de42b..1a181f1a6aeee3a5d47922c52e93d23d2e8978bf 100644
--- a/src/mol-io/reader/gro/parser.ts
+++ b/src/mol-io/reader/gro/parser.ts
@@ -6,10 +6,10 @@
  */
 
 import { Column } from 'mol-data/db'
-import Tokenizer from '../common/text/tokenizer'
+import { Tokenizer } from '../common/text/tokenizer'
 import FixedColumn from '../common/text/column/fixed'
 import * as Schema from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext } from 'mol-task'
 
 interface State {
@@ -155,10 +155,8 @@ async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<
     return Result.success(result);
 }
 
-export function parse(data: string) {
+export function parseGRO(data: string) {
     return Task.create<Result<Schema.GroFile>>('Parse GRO', async ctx => {
         return await parseInternal(data, ctx);
     });
 }
-
-export default parse;
\ No newline at end of file
diff --git a/src/mol-io/reader/mol2/parser.ts b/src/mol-io/reader/mol2/parser.ts
index 297e1502618329594b0966427649ba7b014aaa87..0a11a9a0aa93da4402fcf43f2c16270e4728d16e 100644
--- a/src/mol-io/reader/mol2/parser.ts
+++ b/src/mol-io/reader/mol2/parser.ts
@@ -15,7 +15,7 @@ import { Column } from 'mol-data/db'
 import { TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import TokenColumn from '../common/text/column/token'
 import * as Schema from './schema'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 const { skipWhitespace, eatValue, markLine, getTokenString, readLine } = Tokenizer;
@@ -130,12 +130,12 @@ async function handleAtoms(state: State): Promise<Schema.Mol2Atoms> {
     }
 
     // required columns
-    const atom_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const xTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const yTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const zTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const atom_typeTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
+    const atom_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const atom_nameTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const xTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const yTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const zTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const atom_typeTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
 
     const atom_idTokenColumn = TokenColumn(atom_idTokens);
     const atom_nameTokenColumn = TokenColumn(atom_nameTokens);
@@ -145,10 +145,10 @@ async function handleAtoms(state: State): Promise<Schema.Mol2Atoms> {
     const atom_typeColumn = TokenColumn(atom_typeTokens);
 
     // optional columns
-    const subst_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const subst_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const chargeTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
-    const status_bitTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);
+    const subst_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const subst_nameTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const chargeTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
+    const status_bitTokens = TokenBuilder.create(tokenizer.data, molecule.num_atoms * 2);
 
     const subst_idTokenColumn = TokenColumn(subst_idTokens);
     const subst_nameTokenColumn = TokenColumn(subst_nameTokens);
@@ -257,10 +257,10 @@ async function handleBonds(state: State): Promise<Schema.Mol2Bonds> {
     }
 
     // required columns
-    const bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const origin_bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const target_bond_idTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
-    const bondTypeTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
+    const bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const origin_bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const target_bond_idTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
+    const bondTypeTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
 
     const bond_idTokenColumn = TokenColumn(bond_idTokens);
     const origin_bond_idTokenColumn = TokenColumn(origin_bond_idTokens);
@@ -268,7 +268,7 @@ async function handleBonds(state: State): Promise<Schema.Mol2Bonds> {
     const bondTypeTokenColumn = TokenColumn(bondTypeTokens);
 
     // optional columns
-    const status_bitTokens = TokenBuilder.create(tokenizer, molecule.num_bonds * 2);
+    const status_bitTokens = TokenBuilder.create(tokenizer.data, molecule.num_bonds * 2);
     const status_bitTokenColumn = TokenColumn(status_bitTokens);
     const undefStr = Column.Undefined(molecule.num_bonds, Column.Schema.str);
 
diff --git a/src/mol-io/reader/obj/parser.ts b/src/mol-io/reader/obj/parser.ts
index 046143b305b51530b1e8d0e1cad7f8cb097fa50c..a8b113a33d0c88f5af90189e79359df8b5f097ae 100644
--- a/src/mol-io/reader/obj/parser.ts
+++ b/src/mol-io/reader/obj/parser.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext } from 'mol-task'
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 
diff --git a/src/mol-io/reader/pdb/parser.ts b/src/mol-io/reader/pdb/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..600ac278e0bbf6290e773c028bd5d0140dd5d201
--- /dev/null
+++ b/src/mol-io/reader/pdb/parser.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PdbFile } from './schema';
+import { Task } from 'mol-task';
+import { ReaderResult } from '../result';
+import { Tokenizer } from '../common/text/tokenizer';
+
+export function parsePDB(data: string, id?: string): Task<ReaderResult<PdbFile>> {
+    return Task.create('Parse PDB', async ctx => ReaderResult.success({ id, lines: await Tokenizer.readAllLinesAsync(data, ctx) }));
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/pdb/schema.ts b/src/mol-io/reader/pdb/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3031f1aea0e085a50ec47a7ae352534ef4e2ce4a
--- /dev/null
+++ b/src/mol-io/reader/pdb/schema.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Tokens } from '../common/text/tokenizer';
+
+export interface PdbFile {
+    id?: string,
+    lines: Tokens
+}
\ No newline at end of file
diff --git a/src/mol-io/reader/ply/parse_data/ply_parser.ts b/src/mol-io/reader/ply/parse_data/ply_parser.ts
index 30ceb2d64b943cb75f6b5e9c23e0de8e88c8663f..a9844ee5d2d19c7ecaf1c92f3716091d436b85a5 100644
--- a/src/mol-io/reader/ply/parse_data/ply_parser.ts
+++ b/src/mol-io/reader/ply/parse_data/ply_parser.ts
@@ -7,7 +7,7 @@
 import { Tokens, TokenBuilder, Tokenizer } from '../../common/text/tokenizer'
 import * as Data from './data-model'
 
-import Result from '../../result'
+import{ ReaderResult } from '../../result'
 import {Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 const enum PlyTokenType {
@@ -196,11 +196,11 @@ function moveNextInternal(state: State) {
                     if(state.currentProperty < 3){
                         state.vertices[state.currentVertex * 3 + state.currentProperty] = Number(Tokenizer.getTokenString(state.tokenizer));
                     }
-                    if(state.currentProperty >= 3 && state.currentProperty <7){
+                    if(state.currentProperty >= 3 && state.currentProperty <6){
                         state.colors[state.currentVertex * 3 + state.currentProperty-3] = Number(Tokenizer.getTokenString(state.tokenizer));
                     }
-                    if(state.currentProperty >= 7 && state.currentProperty <10){
-                        state.normals[state.currentVertex * 3 + state.currentProperty-7] = Number(Tokenizer.getTokenString(state.tokenizer));
+                    if(state.currentProperty >= 6 && state.currentProperty <9){
+                        state.normals[state.currentVertex * 3 + state.currentProperty-6] = Number(Tokenizer.getTokenString(state.tokenizer));
                     }
                     state.currentProperty++;
                     if(state.currentProperty === state.propertyCount){
@@ -257,8 +257,10 @@ function readRecordsChunks(state: State) {
 }
 
 function addHeadEntry (state: State) {
-    state.initialHead.push(Tokenizer.getTokenString(state.tokenizer))
-    state.tokens.push(TokenBuilder.create(state.tokenizer, state.data.length / 80))
+    const head = Tokenizer.getTokenString(state.tokenizer)
+    console.log(head)
+    state.initialHead.push(head)
+    state.tokens.push(TokenBuilder.create(head, state.data.length / 80))
 }
 
 
@@ -300,7 +302,7 @@ async function handleRecords(state: State): Promise<Data.ply_form> {
     return Data.PlyStructure(state.vertexCount, state.faceCount, state.propertyCount, state.initialHead, state.propertyNames, state.properties, state.vertices, state.colors, state.normals, state.faces)
 }
 
-async function parseInternal(data: string, ctx: RuntimeContext, opts: PlyOptions): Promise<Result<Data.PlyFile>> {
+async function parseInternal(data: string, ctx: RuntimeContext, opts: PlyOptions): Promise<ReaderResult<Data.PlyFile>> {
     const state = State(data, ctx, opts);
 
     ctx.update({ message: 'Parsing...', current: 0, max: data.length });
@@ -316,7 +318,7 @@ async function parseInternal(data: string, ctx: RuntimeContext, opts: PlyOptions
     // script.src = "../../build/src/mol-model/shape/formarts/ply/plyData_to_shape.js";
     // document.body.appendChild(script);
 
-    return Result.success(result);
+    return ReaderResult.success(result);
 }
 
 interface PlyOptions {
@@ -327,7 +329,7 @@ interface PlyOptions {
 
 export function parse(data: string, opts?: Partial<PlyOptions>) {
     const completeOpts = Object.assign({}, { comment: 'c', property: 'p', element: 'e' }, opts)
-    return Task.create<Result<Data.PlyFile>>('Parse PLY', async ctx => {
+    return Task.create<ReaderResult<Data.PlyFile>>('Parse PLY', async ctx => {
         return await parseInternal(data, ctx, completeOpts);
     });
 }
diff --git a/src/mol-io/reader/ply/read_data/data.ts b/src/mol-io/reader/ply/read_data/data.ts
index 6e0be3ad24636158fc889585c89fc41ecb3aa198..d251173b54aee9ac9f65c1e388a7272ce91e4faf 100644
--- a/src/mol-io/reader/ply/read_data/data.ts
+++ b/src/mol-io/reader/ply/read_data/data.ts
@@ -1,60 +1,60 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
+// /**
+//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+//  *
+//  * @author David Sehnal <david.sehnal@gmail.com>
+//  */
 
-import { PluginStateTransform } from '../../../../mol-plugin/state/objects';
-import { PluginStateObject as SO } from '../../../../mol-plugin/state/objects';
-import { Task } from 'mol-task';
-import PLY from 'mol-io/reader/ply/parse_data/ply_parser'
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Transformer } from 'mol-state';
-import { readFromFile } from './data-source';
+// import { PluginStateTransform } from '../../../../mol-plugin/state/objects';
+// import { PluginStateObject as SO } from '../../../../mol-plugin/state/objects';
+// import { Task } from 'mol-task';
+// import PLY from 'mol-io/reader/ply/parse_data/ply_parser'
+// import { ParamDefinition as PD } from 'mol-util/param-definition';
+// import { Transformer } from 'mol-state';
+// import { readFromFile } from './data-source';
 
-export { ReadFile_ascii }
-type ReadFile_ascii = typeof ReadFile_ascii
-const ReadFile_ascii = PluginStateTransform.BuiltIn({
-    name: 'ReadFile_ascii',
-    display: { name: 'ReadFile_ascii', description: 'Read string data from the specified file' },
-    from: SO.Root,
-    to: [SO.Data.String],
-    params: {
-        file: PD.File(),
-        label: PD.makeOptional(PD.Text('')),
-        isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
-    }
-})({
-    apply({ params: p }) {
-        return Task.create('Open File', async ctx => {
-            const data = await readFromFile(p.file).runInContext(ctx);
-            return  new SO.Data.String(data as string, { label: p.label ? p.label : p.file.name });
-        });
-    },
-    update({ oldParams, newParams, b }) {
-        if (oldParams.label !== newParams.label) {
-            (b.label as string) = newParams.label || oldParams.file.name;
-            return Transformer.UpdateResult.Updated;
-        }
-        return Transformer.UpdateResult.Unchanged;
-    },
-    isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' })
-});
+// export { ReadFile_ascii }
+// type ReadFile_ascii = typeof ReadFile_ascii
+// const ReadFile_ascii = PluginStateTransform.BuiltIn({
+//     name: 'ReadFile_ascii',
+//     display: { name: 'ReadFile_ascii', description: 'Read string data from the specified file' },
+//     from: SO.Root,
+//     to: [SO.Data.String],
+//     params: {
+//         file: PD.File(),
+//         label: PD.Optional(PD.Text('')),
+//         isBinary: PD.Optional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
+//     }
+// })({
+//     apply({ params: p }) {
+//         return Task.create('Open File', async ctx => {
+//             const data = await readFromFile(p.file).runInContext(ctx);
+//             return  new SO.Data.String(data as string, { label: p.label ? p.label : p.file.name });
+//         });
+//     },
+//     update({ oldParams, newParams, b }) {
+//         if (oldParams.label !== newParams.label) {
+//             (b.label as string) = newParams.label || oldParams.file.name;
+//             return Transformer.UpdateResult.Updated;
+//         }
+//         return Transformer.UpdateResult.Unchanged;
+//     },
+//     isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' })
+// });
 
 
-export { ParsePLY }
-type ParsePLY = typeof ParsePLY
-const ParsePLY = PluginStateTransform.BuiltIn({
-    name: 'parse-ply',
-    display: { name: 'Parse PLY', description: 'Parse PLY from String' },
-    from: [SO.Data.String],
-    to: SO.Format.Ply
-})({
-    apply({ a }) {
-        return Task.create('Parse PLY', async ctx => {
-            const parsed = await (PLY(a.data).runInContext(ctx));
-            if (parsed.isError) throw new Error(parsed.message);
-            return new SO.Format.Ply(parsed.result);
-        });
-    }
-});
\ No newline at end of file
+// export { ParsePLY }
+// type ParsePLY = typeof ParsePLY
+// const ParsePLY = PluginStateTransform.BuiltIn({
+//     name: 'parse-ply',
+//     display: { name: 'Parse PLY', description: 'Parse PLY from String' },
+//     from: [SO.Data.String],
+//     to: SO.Format.Ply
+// })({
+//     apply({ a }) {
+//         return Task.create('Parse PLY', async ctx => {
+//             const parsed = await (PLY(a.data).runInContext(ctx));
+//             if (parsed.isError) throw new Error(parsed.message);
+//             return new SO.Format.Ply(parsed.result);
+//         });
+//     }
+// });
\ No newline at end of file
diff --git a/src/mol-io/reader/result.ts b/src/mol-io/reader/result.ts
index 4eb76dd373929b858e09e2de3d7f649abd078f86..255ae0c9eac4e20d0068d2c266db1fa47e696b8c 100644
--- a/src/mol-io/reader/result.ts
+++ b/src/mol-io/reader/result.ts
@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-type ReaderResult<T> = Success<T> | Error
+type ReaderResult<T> = ReaderResult.Success<T> | ReaderResult.Error
 
 namespace ReaderResult {
     export function error<T>(message: string, line = -1): ReaderResult<T> {
@@ -15,28 +15,28 @@ namespace ReaderResult {
     export function success<T>(result: T, warnings: string[] = []): ReaderResult<T> {
         return new Success<T>(result, warnings);
     }
-}
 
-export class Error {
-    isError: true = true;
+    export class Error {
+        isError: true = true;
 
-    toString() {
-        if (this.line >= 0) {
-            return `[Line ${this.line}] ${this.message}`;
+        toString() {
+            if (this.line >= 0) {
+                return `[Line ${this.line}] ${this.message}`;
+            }
+            return this.message;
         }
-        return this.message;
-    }
 
-    constructor(
-        public message: string,
-        public line: number) {
+        constructor(
+            public message: string,
+            public line: number) {
+        }
     }
-}
 
-export class Success<T> {
-    isError: false = false;
+    export class Success<T> {
+        isError: false = false;
 
-    constructor(public result: T, public warnings: string[]) { }
+        constructor(public result: T, public warnings: string[]) { }
+    }
 }
 
-export default ReaderResult
\ No newline at end of file
+export { ReaderResult }
\ No newline at end of file
diff --git a/src/mol-math/geometry/common.ts b/src/mol-math/geometry/common.ts
index 6eab9cfa2492ef2a0cc1b95145ab8fa646805dd3..674e3ede99369615562c0a8056b8735e43390c05 100644
--- a/src/mol-math/geometry/common.ts
+++ b/src/mol-math/geometry/common.ts
@@ -14,9 +14,9 @@ export interface PositionData {
     x: ArrayLike<number>,
     y: ArrayLike<number>,
     z: ArrayLike<number>,
-    // subset indices into the x/y/z/radius arrays
+    /** subset of indices into the x/y/z/radius arrays */
     indices: OrderedSet,
-    // optional element radius
+    /** optional element radius */
     radius?: ArrayLike<number>
 }
 
diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts
index 17dfac6f8a379d6729d6a251b46eec0982bdef34..2f91bcd1af1f8228fc22c7979abb0a1493b63a13 100644
--- a/src/mol-math/geometry/gaussian-density/gpu.ts
+++ b/src/mol-math/geometry/gaussian-density/gpu.ts
@@ -232,6 +232,7 @@ function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number,
     }
     const state: RenderableState = {
         visible: true,
+        alphaFactor: 1,
         pickable: false,
         opaque: true
     }
diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts
index aa9393220b348ac1b0a206fc464bf95d38ca21b5..5270a27600e482edf597e63ec2b821116a16ab47 100644
--- a/src/mol-math/geometry/lookup3d/grid.ts
+++ b/src/mol-math/geometry/lookup3d/grid.ts
@@ -17,8 +17,8 @@ interface GridLookup3D<T = number> extends Lookup3D<T> {
     readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
 }
 
-function GridLookup3D(data: PositionData, cellSize?: Vec3): GridLookup3D {
-    return new GridLookup3DImpl(data, cellSize);
+function GridLookup3D(data: PositionData, cellSizeOrCount?: Vec3 | number): GridLookup3D {
+    return new GridLookup3DImpl(data, cellSizeOrCount);
 }
 
 export { GridLookup3D }
@@ -47,8 +47,8 @@ class GridLookup3DImpl implements GridLookup3D<number> {
         return query(this.ctx);
     }
 
-    constructor(data: PositionData, cellSize?: Vec3) {
-        const structure = build(data, cellSize);
+    constructor(data: PositionData, cellSizeOrCount?: Vec3 | number) {
+        const structure = build(data, cellSizeOrCount);
         this.ctx = createContext(structure);
         this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
         this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
@@ -184,7 +184,7 @@ function getBoundary(data: PositionData) {
     return { boundingBox: boundaryHelper.getBox(), boundingSphere: boundaryHelper.getSphere() };
 }
 
-function build(data: PositionData, cellSize?: Vec3) {
+function build(data: PositionData, cellSizeOrCount?: Vec3 | number) {
     const { boundingBox, boundingSphere } = getBoundary(data);
     // need to expand the grid bounds to avoid rounding errors
     const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
@@ -195,13 +195,16 @@ function build(data: PositionData, cellSize?: Vec3) {
 
     const elementCount = OrderedSet.size(indices);
 
+    const cellCount = typeof cellSizeOrCount === 'number' ? cellSizeOrCount : 32
+    const cellSize = Array.isArray(cellSizeOrCount) && cellSizeOrCount
+
     if (cellSize) {
         size = [Math.ceil(S[0] / cellSize[0]), Math.ceil(S[1] / cellSize[1]), Math.ceil(S[2] / cellSize[2])];
         delta = cellSize;
     } else if (elementCount > 0) {
         // size of the box
-        // required "grid volume" so that each cell contains on average 32 elements.
-        const V = Math.ceil(elementCount / 32);
+        // required "grid volume" so that each cell contains on average 'cellCount' elements.
+        const V = Math.ceil(elementCount / cellCount);
         const f = Math.pow(V / (S[0] * S[1] * S[2]), 1 / 3);
         size = [Math.ceil(S[0] * f), Math.ceil(S[1] * f), Math.ceil(S[2] * f)];
         delta = [S[0] / size[0], S[1] / size[1], S[2] / size[2]];
diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts
index 563d10ea7cb362e67da09a624e02b5eca772b259..83c352cb5c64a908ef2b98997b9e28946e73c9ca 100644
--- a/src/mol-math/geometry/primitives/box3d.ts
+++ b/src/mol-math/geometry/primitives/box3d.ts
@@ -44,6 +44,13 @@ namespace Box3D {
         return Vec3.sub(size, box.max, box.min);
     }
 
+    const tmpSizeV = Vec3.zero()
+    /** Get size of the box */
+    export function volume(box: Box3D): number {
+        size(tmpSizeV, box)
+        return tmpSizeV[0] * tmpSizeV[1] * tmpSizeV[2]
+    }
+
     export function setEmpty(box: Box3D): Box3D {
         Vec3.set(box.min, Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
         Vec3.set(box.max, -Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
diff --git a/src/mol-math/geometry/spacegroup/construction.ts b/src/mol-math/geometry/spacegroup/construction.ts
index 076a849f60beaf59ffbb365601457b5a45695236..a347c60e7307fade102bacacb931788d21f6b4b0 100644
--- a/src/mol-math/geometry/spacegroup/construction.ts
+++ b/src/mol-math/geometry/spacegroup/construction.ts
@@ -90,7 +90,7 @@ namespace Spacegroup {
 
     export function getSymmetryOperator(spacegroup: Spacegroup, index: number, i: number, j: number, k: number): SymmetryOperator {
         const operator = updateOperatorMatrix(spacegroup, index, i, j, k, Mat4.zero());
-        return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, Vec3.create(i, j, k));
+        return SymmetryOperator.create(`${index + 1}_${5 + i}${5 + j}${5 + k}`, operator, { id: '', operList: [] }, '', Vec3.create(i, j, k));
     }
 
     function getOperatorMatrix(ids: number[]) {
diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts
index 0c8297c8cef93c0b65b02e8fc67ba6db18ec3cd3..44ba2975516385dcf35260e7eddc971dd48c80b6 100644
--- a/src/mol-math/geometry/symmetry-operator.ts
+++ b/src/mol-math/geometry/symmetry-operator.ts
@@ -4,18 +4,21 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Vec3, Mat4, Mat3 } from '../linear-algebra/3d'
+import { Vec3, Mat4, Mat3, Quat } from '../linear-algebra/3d'
 
 interface SymmetryOperator {
     readonly name: string,
 
     readonly assembly: {
-        /** pointer to `pdbx_struct_assembly.id` */
+        /** pointer to `pdbx_struct_assembly.id` or empty string */
         readonly id: string
-        /** pointers to `pdbx_struct_oper_list_id` */
+        /** pointers to `pdbx_struct_oper_list.id` or empty list */
         readonly operList: string[]
     }
 
+    /** pointer to `struct_ncs_oper.id` or empty string */
+    readonly ncsId: string,
+
     readonly hkl: Vec3,
 
     readonly matrix: Mat4,
@@ -31,11 +34,12 @@ namespace SymmetryOperator {
 
     const RotationEpsilon = 0.0001;
 
-    export function create(name: string, matrix: Mat4, assembly: SymmetryOperator['assembly'], hkl?: Vec3): SymmetryOperator {
+    export function create(name: string, matrix: Mat4, assembly: SymmetryOperator['assembly'], ncsId?: string, hkl?: Vec3): SymmetryOperator {
         const _hkl = hkl ? Vec3.clone(hkl) : Vec3.zero();
-        if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl };
+        ncsId = ncsId || ''
+        if (Mat4.isIdentity(matrix)) return { name, assembly, matrix, inverse: Mat4.identity(), isIdentity: true, hkl: _hkl, ncsId };
         if (!Mat4.isRotationAndTranslation(matrix, RotationEpsilon)) throw new Error(`Symmetry operator (${name}) must be a composition of rotation and translation.`);
-        return { name, assembly, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl };
+        return { name, assembly, matrix, inverse: Mat4.invert(Mat4.zero(), matrix), isIdentity: false, hkl: _hkl, ncsId };
     }
 
     export function checkIfRotationAndTranslation(rot: Mat3, offset: Vec3) {
@@ -49,7 +53,7 @@ namespace SymmetryOperator {
         return Mat4.isRotationAndTranslation(matrix, RotationEpsilon);
     }
 
-    export function ofRotationAndOffset(name: string, rot: Mat3, offset: Vec3) {
+    export function ofRotationAndOffset(name: string, rot: Mat3, offset: Vec3, ncsId?: string) {
         const t = Mat4.identity();
         for (let i = 0; i < 3; i++) {
             for (let j = 0; j < 3; j++) {
@@ -57,16 +61,36 @@ namespace SymmetryOperator {
             }
         }
         Mat4.setTranslation(t, offset);
-        return create(name, t, { id: '', operList: [] });
+        return create(name, t, { id: '', operList: [] }, ncsId);
+    }
+
+    const _q1 = Quat.identity(), _q2 = Quat.zero(), _axis = Vec3.zero();
+    export function lerpFromIdentity(out: Mat4, op: SymmetryOperator, t: number): Mat4 {
+        const m = op.inverse;
+        if (op.isIdentity) return Mat4.copy(out, m);
+
+        const _t = 1 - t;
+        // interpolate rotation
+        Mat4.getRotation(_q2, m);
+        Quat.slerp(_q2, _q1, _q2, _t);
+        const angle = Quat.getAxisAngle(_axis, _q2);
+        Mat4.fromRotation(out, angle, _axis);
+
+        // interpolate translation
+        Mat4.setValue(out, 0, 3, _t * Mat4.getValue(m, 0, 3));
+        Mat4.setValue(out, 1, 3, _t * Mat4.getValue(m, 1, 3));
+        Mat4.setValue(out, 2, 3, _t * Mat4.getValue(m, 2, 3));
+
+        return out;
     }
 
     /**
      * Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix).
-     * Keep `name`, `assembly` and `hkl` properties from second.
+     * Keep `name`, `assembly`, `ncsId` and `hkl` properties from second.
      */
     export function compose(first: SymmetryOperator, second: SymmetryOperator) {
         const matrix = Mat4.mul(Mat4.zero(), second.matrix, first.matrix);
-        return create(second.name, matrix, second.assembly, second.hkl);
+        return create(second.name, matrix, second.assembly, second.ncsId, second.hkl);
     }
 
     export interface CoordinateMapper<T extends number> { (index: T, slot: Vec3): Vec3 }
diff --git a/src/mol-math/graph/int-adjacency-graph.ts b/src/mol-math/graph/int-adjacency-graph.ts
index 8b4c5b62f6fd60c03db3bc0c785f5ec409fccb9a..efd567b2e54d76084c74fb7f990b715c5cb44a86 100644
--- a/src/mol-math/graph/int-adjacency-graph.ts
+++ b/src/mol-math/graph/int-adjacency-graph.ts
@@ -160,6 +160,75 @@ export namespace IntAdjacencyGraph {
         }
     }
 
+    export class DirectedEdgeBuilder {
+        private bucketFill: Int32Array;
+        private current = 0;
+        private curA: number = 0;
+
+        offsets: Int32Array;
+        edgeCount: number;
+        /** the size of the A and B arrays */
+        slotCount: number;
+        a: Int32Array;
+        b: Int32Array;
+
+        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase = {}>(edgeProps?: EdgeProps) {
+            return create(this.offsets, this.a, this.b, this.edgeCount, edgeProps);
+        }
+
+        /**
+         * @example
+         *   const property = new Int32Array(builder.slotCount);
+         *   for (let i = 0; i < builder.edgeCount; i++) {
+         *     builder.addNextEdge();
+         *     builder.assignProperty(property, srcProp[i]);
+         *   }
+         *   return builder.createGraph({ property });
+         */
+        addNextEdge() {
+            const a = this.xs[this.current], b = this.ys[this.current];
+
+            const oa = this.offsets[a] + this.bucketFill[a];
+
+            this.a[oa] = a;
+            this.b[oa] = b;
+            this.bucketFill[a]++;
+
+            this.current++;
+            this.curA = oa;
+        }
+
+        /** Builds property-less graph */
+        addAllEdges() {
+            for (let i = 0; i < this.edgeCount; i++) {
+                this.addNextEdge();
+            }
+        }
+
+        assignProperty<T>(prop: { [i: number]: T }, value: T) {
+            prop[this.curA] = value;
+        }
+
+        constructor(public vertexCount: number, public xs: ArrayLike<number>, public ys: ArrayLike<number>) {
+            this.edgeCount = xs.length;
+            this.offsets = new Int32Array(this.vertexCount + 1);
+            this.bucketFill = new Int32Array(this.vertexCount);
+
+            const bucketSizes = new Int32Array(this.vertexCount);
+            for (let i = 0, _i = this.xs.length; i < _i; i++) bucketSizes[this.xs[i]]++;
+
+            let offset = 0;
+            for (let i = 0; i < this.vertexCount; i++) {
+                this.offsets[i] = offset;
+                offset += bucketSizes[i];
+            }
+            this.offsets[this.vertexCount] = offset;
+            this.slotCount = offset;
+            this.a = new Int32Array(offset);
+            this.b = new Int32Array(offset);
+        }
+    }
+
     export class UniqueEdgeBuilder {
         private xs: number[] = [];
         private ys: number[] = [];
diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts
index b18fb3e5cc57036ca1ea9d69a9dba0f5162bf7cb..c8a307e8de9a57ab893dab678ce1bff9d1203076 100644
--- a/src/mol-math/linear-algebra/3d/mat4.ts
+++ b/src/mol-math/linear-algebra/3d/mat4.ts
@@ -22,6 +22,7 @@ import Vec3 from './vec3';
 import Quat from './quat';
 import { degToRad } from '../../misc';
 import { NumberArray } from 'mol-util/type-helpers';
+import Mat3 from './mat3';
 
 interface Mat4 extends Array<number> { [d: number]: number, '@type': 'mat4', length: 16 }
 interface ReadonlyMat4 extends Array<number> { readonly [d: number]: number, '@type': 'mat4', length: 16 }
@@ -119,6 +120,10 @@ namespace Mat4 {
         a[4 * j + i] = value;
     }
 
+    export function getValue(a: Mat4, i: number, j: number) {
+        return a[4 * j + i];
+    }
+
     export function toArray(a: Mat4, out: NumberArray, offset: number) {
         out[offset + 0] = a[0];
         out[offset + 1] = a[1];
@@ -657,6 +662,22 @@ namespace Mat4 {
         return out;
     }
 
+    /**
+     * Copies the mat3 into upper-left 3x3 values.
+     */
+    export function fromMat3(out: Mat4, a: Mat3) {
+        out[0] = a[0];
+        out[1] = a[1];
+        out[2] = a[2];
+        out[4] = a[3];
+        out[5] = a[4];
+        out[6] = a[5];
+        out[8] = a[6];
+        out[9] = a[7];
+        out[10] = a[8];
+        return out;
+    }
+
     export function makeTable(m: Mat4) {
         let ret = '';
         for (let i = 0; i < 4; i++) {
diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts
index 0be0f033dcbff1020f4ea5f9ac22566f45daf100..3e8fd5d5ee7765fcb88215a1455655b75357b10e 100644
--- a/src/mol-math/linear-algebra/3d/vec3.ts
+++ b/src/mol-math/linear-algebra/3d/vec3.ts
@@ -452,9 +452,14 @@ namespace Vec3 {
     }
 
     const rotTemp = zero();
+    const flipScaling = create(-1, -1, -1);
     export function makeRotation(mat: Mat4, a: Vec3, b: Vec3): Mat4 {
         const by = angle(a, b);
         if (Math.abs(by) < 0.0001) return Mat4.setIdentity(mat);
+        if (Math.abs(by - Math.PI) < EPSILON.Value) {
+            // here, axis can be [0,0,0] but the rotation is a simple flip
+            return Mat4.fromScaling(mat, flipScaling);
+        }
         const axis = cross(rotTemp, a, b);
         return Mat4.fromRotation(mat, by, axis);
     }
diff --git a/src/mol-model-formats/structure/format.ts b/src/mol-model-formats/structure/format.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d8ecc3d822830da08eb6718c50166457d89d644a
--- /dev/null
+++ b/src/mol-model-formats/structure/format.ts
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
+import CIF, { CifFrame } from 'mol-io/reader/cif';
+
+type ModelFormat =
+    | ModelFormat.mmCIF
+
+namespace ModelFormat {
+    export interface mmCIF { kind: 'mmCIF', data: mmCIF_Database, frame: CifFrame }
+    export function mmCIF(frame: CifFrame, data?: mmCIF_Database): mmCIF { return { kind: 'mmCIF', data: data || CIF.schema.mmCIF(frame), frame }; }
+}
+
+export { ModelFormat }
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/gro.ts b/src/mol-model-formats/structure/gro.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bd75839a3008eba2d73005d55155c0f40d210e08
--- /dev/null
+++ b/src/mol-model-formats/structure/gro.ts
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Model } from 'mol-model/structure/model';
+import { Task } from 'mol-task';
+import { ModelFormat } from './format';
+import { _parse_mmCif } from './mmcif/parser';
+import { GroFile, GroAtoms } from 'mol-io/reader/gro/schema';
+import { CifCategory, CifField } from 'mol-io/reader/cif';
+import { Column } from 'mol-data/db';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+
+// TODO multi model files
+// TODO seperate chains
+// TODO better entity handling
+// TODO improve performance
+
+function _entity(): { [K in keyof mmCIF_Schema['entity']]?: CifField } {
+    return {
+        id: CifField.ofStrings(['1', '2', '3']),
+        type: CifField.ofStrings(['polymer', 'non-polymer', 'water'])
+    }
+}
+
+function _atom_site(atoms: GroAtoms): { [K in keyof mmCIF_Schema['atom_site']]?: CifField } {
+    const auth_asym_id = CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str))
+    const auth_atom_id = CifField.ofColumn(atoms.atomName)
+    const auth_comp_id = CifField.ofColumn(atoms.residueName)
+    const auth_seq_id = CifField.ofColumn(atoms.residueNumber)
+
+    return {
+        auth_asym_id,
+        auth_atom_id,
+        auth_comp_id,
+        auth_seq_id,
+        B_iso_or_equiv: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.float)),
+        Cartn_x: CifField.ofNumbers(Column.mapToArray(atoms.x, x => x * 10, Float32Array)), 
+        Cartn_y: CifField.ofNumbers(Column.mapToArray(atoms.y, y => y * 10, Float32Array)),
+        Cartn_z: CifField.ofNumbers(Column.mapToArray(atoms.z, z => z * 10, Float32Array)),
+        group_PDB: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+        id: CifField.ofColumn(atoms.atomNumber),
+
+        label_alt_id: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+
+        label_asym_id: auth_asym_id,
+        label_atom_id: auth_atom_id,
+        label_comp_id: auth_comp_id,
+        label_seq_id: auth_seq_id,
+        label_entity_id: CifField.ofColumn(Column.ofConst('1', atoms.count, Column.Schema.str)),
+
+        occupancy: CifField.ofColumn(Column.ofConst(1, atoms.count, Column.Schema.float)),
+        type_symbol: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+
+        pdbx_PDB_ins_code: CifField.ofColumn(Column.Undefined(atoms.count, Column.Schema.str)),
+        pdbx_PDB_model_num: CifField.ofColumn(Column.ofConst('1', atoms.count, Column.Schema.str)),
+    }
+}
+
+async function groToMmCif(gro: GroFile) {
+    const categories = {
+        entity: CifCategory.ofFields('entity', _entity()),
+        atom_site: CifCategory.ofFields('atom_site', _atom_site(gro.structures[0].atoms))
+    } as any;
+
+    return {
+        header: 'GRO',
+        categoryNames: Object.keys(categories),
+        categories
+    };
+}
+
+export function trajectoryFromGRO(gro: GroFile): Task<Model.Trajectory> {
+    return Task.create('Parse GRO', async ctx => {
+        await ctx.update('Converting to mmCIF');
+        const cif = await groToMmCif(gro);
+        const format = ModelFormat.mmCIF(cif);
+        return _parse_mmCif(format, ctx);
+    })
+}
diff --git a/src/mol-model-formats/structure/mmcif.ts b/src/mol-model-formats/structure/mmcif.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6eecc06b6b3e94ae8e5da0aa09141a4582fdb15c
--- /dev/null
+++ b/src/mol-model-formats/structure/mmcif.ts
@@ -0,0 +1,16 @@
+/**
+ * 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 { Model } from 'mol-model/structure/model/model';
+import { Task } from 'mol-task';
+import { ModelFormat } from './format';
+import { _parse_mmCif } from './mmcif/parser';
+import { CifFrame } from 'mol-io/reader/cif';
+
+export function trajectoryFromMmCIF(frame: CifFrame): Task<Model.Trajectory> {
+    return Task.create('Create mmCIF Model', ctx => _parse_mmCif(ModelFormat.mmCIF(frame), ctx));
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif/assembly.ts b/src/mol-model-formats/structure/mmcif/assembly.ts
similarity index 94%
rename from src/mol-model/structure/model/formats/mmcif/assembly.ts
rename to src/mol-model-formats/structure/mmcif/assembly.ts
index d0c9eddeac44e99333be0e4f0b77c14d189cfe56..ad0fa39b9b9cf687478b2d6b5fc6730b4ea5bc03 100644
--- a/src/mol-model/structure/model/formats/mmcif/assembly.ts
+++ b/src/mol-model-formats/structure/mmcif/assembly.ts
@@ -6,12 +6,11 @@
 
 import { Mat4, Tensor } from 'mol-math/linear-algebra'
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
-import Format from '../../format'
-import { Assembly, OperatorGroup, OperatorGroups } from '../../properties/symmetry'
-import { Queries as Q } from '../../../query'
-
-import mmCIF_Format = Format.mmCIF
-import { StructureProperties } from '../../../structure';
+import { Assembly, OperatorGroup, OperatorGroups } from 'mol-model/structure/model/properties/symmetry'
+import { Queries as Q } from 'mol-model/structure'
+import { StructureProperties } from 'mol-model/structure';
+import { ModelFormat } from '../format';
+import mmCIF_Format = ModelFormat.mmCIF
 
 export function createAssemblies(format: mmCIF_Format): ReadonlyArray<Assembly> {
     const { pdbx_struct_assembly } = format.data;
diff --git a/src/mol-model/structure/model/formats/mmcif/atomic.ts b/src/mol-model-formats/structure/mmcif/atomic.ts
similarity index 77%
rename from src/mol-model/structure/model/formats/mmcif/atomic.ts
rename to src/mol-model-formats/structure/mmcif/atomic.ts
index 8ae819b489f88f991e96dac5701c74f9699e8004..38e39f5b63ad2108e61d1f1cbadac5e78b21154c 100644
--- a/src/mol-model/structure/model/formats/mmcif/atomic.ts
+++ b/src/mol-model-formats/structure/mmcif/atomic.ts
@@ -8,18 +8,15 @@ import { Column, Table } from 'mol-data/db';
 import { Interval, Segmentation } from 'mol-data/int';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import UUID from 'mol-util/uuid';
-import { ElementIndex } from '../../../../structure';
-import Format from '../../format';
-import { Model } from '../../model';
-import { AtomicConformation, AtomicData, AtomicHierarchy, AtomicSegments, AtomsSchema, ChainsSchema, ResiduesSchema } from '../../properties/atomic';
-import { getAtomicIndex } from '../../properties/utils/atomic-index';
-import { ElementSymbol } from '../../types';
-import { Entities } from '../../properties/common';
-
-import mmCIF_Format = Format.mmCIF
-import { getAtomicRanges } from '../../properties/utils/atomic-ranges';
-import { FormatData } from '../mmcif';
-import { getAtomicDerivedData } from '../../properties/utils/atomic-derived';
+import { ElementIndex } from 'mol-model/structure';
+import { Model } from 'mol-model/structure/model/model';
+import { AtomicConformation, AtomicData, AtomicHierarchy, AtomicSegments, AtomsSchema, ChainsSchema, ResiduesSchema } from 'mol-model/structure/model/properties/atomic';
+import { getAtomicIndex } from 'mol-model/structure/model/properties/utils/atomic-index';
+import { ElementSymbol } from 'mol-model/structure/model/types';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { getAtomicRanges } from 'mol-model/structure/model/properties/utils/atomic-ranges';
+import { getAtomicDerivedData } from 'mol-model/structure/model/properties/utils/atomic-derived';
+import { FormatData } from './parser';
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -45,13 +42,14 @@ function findHierarchyOffsets(atom_site: AtomSite) {
     return { residues, chains };
 }
 
-function createHierarchyData(atom_site: AtomSite, offsets: { residues: ArrayLike<number>, chains: ArrayLike<number> }): AtomicData {
+function createHierarchyData(atom_site: AtomSite, sourceIndex: Column<number>, offsets: { residues: ArrayLike<number>, chains: ArrayLike<number> }): AtomicData {
     const atoms = Table.ofColumns(AtomsSchema, {
         type_symbol: Column.ofArray({ array: Column.mapToArray(atom_site.type_symbol, ElementSymbol), schema: Column.Schema.Aliased<ElementSymbol>(Column.Schema.str) }),
         label_atom_id: atom_site.label_atom_id,
         auth_atom_id: atom_site.auth_atom_id,
         label_alt_id: atom_site.label_alt_id,
-        pdbx_formal_charge: atom_site.pdbx_formal_charge
+        pdbx_formal_charge: atom_site.pdbx_formal_charge,
+        sourceIndex
     });
     const residues = Table.view(atom_site, ResiduesSchema, offsets.residues);
     // Optimize the numeric columns
@@ -80,9 +78,9 @@ function isHierarchyDataEqual(a: AtomicData, b: AtomicData) {
         && Table.areEqual(a.atoms as Table<AtomsSchema>, b.atoms as Table<AtomsSchema>)
 }
 
-export function getAtomicHierarchyAndConformation(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, formatData: FormatData, previous?: Model) {
+export function getAtomicHierarchyAndConformation(atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model) {
     const hierarchyOffsets = findHierarchyOffsets(atom_site);
-    const hierarchyData = createHierarchyData(atom_site, hierarchyOffsets);
+    const hierarchyData = createHierarchyData(atom_site, sourceIndex, hierarchyOffsets);
 
     if (previous && isHierarchyDataEqual(previous.atomicHierarchy, hierarchyData)) {
         return {
@@ -101,7 +99,7 @@ export function getAtomicHierarchyAndConformation(format: mmCIF_Format, atom_sit
 
     const index = getAtomicIndex(hierarchyData, entities, hierarchySegments);
     const derived = getAtomicDerivedData(hierarchyData, index, formatData.chemicalComponentMap);
-    const hierarchyRanges = getAtomicRanges(hierarchyData, hierarchySegments, conformation, formatData.chemicalComponentMap);
+    const hierarchyRanges = getAtomicRanges(hierarchyData, hierarchySegments, conformation, index, derived.residue.moleculeType);
     const hierarchy: AtomicHierarchy = { ...hierarchyData, ...hierarchySegments, ...hierarchyRanges, index, derived };
     return { sameAsPrevious: false, hierarchy, conformation };
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif/bonds.ts b/src/mol-model-formats/structure/mmcif/bonds.ts
similarity index 100%
rename from src/mol-model/structure/model/formats/mmcif/bonds.ts
rename to src/mol-model-formats/structure/mmcif/bonds.ts
diff --git a/src/mol-model/structure/model/formats/mmcif/bonds/comp.ts b/src/mol-model-formats/structure/mmcif/bonds/comp.ts
similarity index 96%
rename from src/mol-model/structure/model/formats/mmcif/bonds/comp.ts
rename to src/mol-model-formats/structure/mmcif/bonds/comp.ts
index 4304807338aded73a011f2d8bbf8100fd276cf18..eceb64760607d51d544a8a9a587a46471bd9334d 100644
--- a/src/mol-model/structure/model/formats/mmcif/bonds/comp.ts
+++ b/src/mol-model-formats/structure/mmcif/bonds/comp.ts
@@ -5,11 +5,11 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
-import { LinkType } from '../../../types'
-import { ModelPropertyDescriptor } from '../../../properties/custom';
+import { Model } from 'mol-model/structure/model/model'
+import { LinkType } from 'mol-model/structure/model/types'
+import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
-import { Structure, Unit, StructureProperties, StructureElement } from '../../../../structure';
+import { Structure, Unit, StructureProperties, StructureElement } from 'mol-model/structure';
 import { Segmentation } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
 
diff --git a/src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
similarity index 93%
rename from src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts
rename to src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
index 486c071a1de7c2bc4f5f9b1e144add4b9187512c..91c819e5a3b918e854029231df48cea4e3619fad 100644
--- a/src/mol-model/structure/model/formats/mmcif/bonds/struct_conn.ts
+++ b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts
@@ -5,16 +5,16 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
-import { Structure } from '../../../../structure'
-import { LinkType } from '../../../types'
+import { Model } from 'mol-model/structure/model/model'
+import { Structure } from 'mol-model/structure'
+import { LinkType } from 'mol-model/structure/model/types'
 import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util'
 import { Column } from 'mol-data/db'
-import { ModelPropertyDescriptor } from '../../../properties/custom';
+import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { SortedArray } from 'mol-data/int';
 import { CifWriter } from 'mol-io/writer/cif'
-import { ElementIndex, ResidueIndex } from '../../../indexing';
+import { ElementIndex, ResidueIndex } from 'mol-model/structure/model/indexing';
 
 export interface StructConn {
     getResidueEntries(residueAIndex: ResidueIndex, residueBIndex: ResidueIndex): ReadonlyArray<StructConn.Entry>,
@@ -138,8 +138,6 @@ export namespace StructConn {
         partners: { residueIndex: ResidueIndex, atomIndex: ElementIndex, symmetry: string }[]
     }
 
-    type StructConnType = typeof mmCIF_Schema.struct_conn.conn_type_id.T
-
     export function attachFromMmCif(model: Model): boolean {
         if (model.customProperties.has(Descriptor)) return true;
         if (model.sourceData.kind !== 'mmCIF') return false;
@@ -213,7 +211,7 @@ export namespace StructConn {
             const partners = _ps(i);
             if (partners.length < 2) continue;
 
-            const type = conn_type_id.value(i)! as StructConnType;
+            const type = conn_type_id.value(i) as typeof mmCIF_Schema.struct_conn_type.id.T; // TODO workaround for dictionary inconsistency
             const orderType = (pdbx_value_order.value(i) || '').toLowerCase();
             let flags = LinkType.Flag.None;
             let order = 1;
@@ -234,7 +232,10 @@ export namespace StructConn {
                     flags = LinkType.Flag.Covalent;
                     break;
                 case 'disulf': flags = LinkType.Flag.Covalent | LinkType.Flag.Sulfide; break;
-                case 'hydrog': flags = LinkType.Flag.Hydrogen; break;
+                case 'hydrog':
+                case 'mismat':
+                    flags = LinkType.Flag.Hydrogen;
+                    break;
                 case 'metalc': flags = LinkType.Flag.MetallicCoordination; break;
                 case 'saltbr': flags = LinkType.Flag.Ionic; break;
             }
diff --git a/src/mol-model/structure/model/formats/mmcif/ihm.ts b/src/mol-model-formats/structure/mmcif/ihm.ts
similarity index 88%
rename from src/mol-model/structure/model/formats/mmcif/ihm.ts
rename to src/mol-model-formats/structure/mmcif/ihm.ts
index 8cba685fb7c13ecaed3ca2c39cb91851b0d81534..405fec8cf0fab7b019f477e41ac0dea1d61a9716 100644
--- a/src/mol-model/structure/model/formats/mmcif/ihm.ts
+++ b/src/mol-model-formats/structure/mmcif/ihm.ts
@@ -5,22 +5,23 @@
  */
 
 import { mmCIF_Database as mmCIF, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'
-import { CoarseHierarchy, CoarseConformation, CoarseElementData, CoarseSphereConformation, CoarseGaussianConformation } from '../../properties/coarse'
-import { Entities } from '../../properties/common';
+import { CoarseHierarchy, CoarseConformation, CoarseElementData, CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse'
+import { Entities } from 'mol-model/structure/model/properties/common';
 import { Column } from 'mol-data/db';
-import { getCoarseKeys } from '../../properties/utils/coarse-keys';
+import { getCoarseKeys } from 'mol-model/structure/model/properties/utils/coarse-keys';
 import { UUID } from 'mol-util';
 import { Segmentation, Interval } from 'mol-data/int';
 import { Mat3, Tensor } from 'mol-math/linear-algebra';
-import { ElementIndex, ChainIndex } from '../../indexing';
-import { getCoarseRanges } from '../../properties/utils/coarse-ranges';
-import { FormatData } from '../mmcif';
+import { ElementIndex, ChainIndex } from 'mol-model/structure/model/indexing';
+import { getCoarseRanges } from 'mol-model/structure/model/properties/utils/coarse-ranges';
+import { FormatData } from './parser';
 
 export interface IHMData {
     model_id: number,
     model_name: string,
     entities: Entities,
     atom_site: mmCIF['atom_site'],
+    atom_site_sourceIndex: Column<number>,
     ihm_sphere_obj_site: mmCIF['ihm_sphere_obj_site'],
     ihm_gaussian_obj_site: mmCIF['ihm_gaussian_obj_site']
 }
diff --git a/src/mol-model/structure/model/formats/mmcif/pair-restraint.ts b/src/mol-model-formats/structure/mmcif/pair-restraint.ts
similarity index 100%
rename from src/mol-model/structure/model/formats/mmcif/pair-restraint.ts
rename to src/mol-model-formats/structure/mmcif/pair-restraint.ts
diff --git a/src/mol-model/structure/model/formats/mmcif/pair-restraints/cross-links.ts b/src/mol-model-formats/structure/mmcif/pair-restraints/cross-links.ts
similarity index 96%
rename from src/mol-model/structure/model/formats/mmcif/pair-restraints/cross-links.ts
rename to src/mol-model-formats/structure/mmcif/pair-restraints/cross-links.ts
index 0aebfc74fe1146a789d35b54804064dc5c3acf66..2964745e9b5723e3621f97551592d9961e1277df 100644
--- a/src/mol-model/structure/model/formats/mmcif/pair-restraints/cross-links.ts
+++ b/src/mol-model-formats/structure/mmcif/pair-restraints/cross-links.ts
@@ -4,12 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../../model'
+import { Model } from 'mol-model/structure/model/model'
 import { Table } from 'mol-data/db'
 import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { findAtomIndexByLabelName } from '../util';
-import { Unit } from '../../../../structure';
-import { ElementIndex } from '../../../indexing';
+import { Unit } from 'mol-model/structure';
+import { ElementIndex } from 'mol-model/structure/model/indexing';
 
 function findAtomIndex(model: Model, entityId: string, asymId: string, seqId: number, atomId: string) {
     if (!model.atomicHierarchy.atoms.auth_atom_id.isDefined) return -1
diff --git a/src/mol-model/structure/model/formats/mmcif/pair-restraints/predicted-contacts.ts b/src/mol-model-formats/structure/mmcif/pair-restraints/predicted-contacts.ts
similarity index 100%
rename from src/mol-model/structure/model/formats/mmcif/pair-restraints/predicted-contacts.ts
rename to src/mol-model-formats/structure/mmcif/pair-restraints/predicted-contacts.ts
diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model-formats/structure/mmcif/parser.ts
similarity index 68%
rename from src/mol-model/structure/model/formats/mmcif.ts
rename to src/mol-model-formats/structure/mmcif/parser.ts
index d0684dcb4a9ca00281cbfc5e82d920f9e50eb632..d31b63d1a8a6d0eaa4368ee6660178a237f0f59b 100644
--- a/src/mol-model/structure/model/formats/mmcif.ts
+++ b/src/mol-model-formats/structure/mmcif/parser.ts
@@ -9,26 +9,32 @@ import { Column, Table } from 'mol-data/db';
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
 import { Spacegroup, SpacegroupCell, SymmetryOperator } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
-import { Task, RuntimeContext } from 'mol-task';
+import { RuntimeContext } from 'mol-task';
 import UUID from 'mol-util/uuid';
-import Format from '../format';
-import { Model } from '../model';
-import { Entities } from '../properties/common';
-import { CustomProperties } from '../properties/custom';
-import { ModelSymmetry } from '../properties/symmetry';
-import { createAssemblies } from './mmcif/assembly';
-import { getAtomicHierarchyAndConformation } from './mmcif/atomic';
-import { ComponentBond } from './mmcif/bonds';
-import { getIHMCoarse, EmptyIHMCoarse, IHMData } from './mmcif/ihm';
-import { getSecondaryStructureMmCif } from './mmcif/secondary-structure';
-import { getSequence } from './mmcif/sequence';
-import { sortAtomSite } from './mmcif/sort';
-import { StructConn } from './mmcif/bonds/struct_conn';
-import { ChemicalComponent, ChemicalComponentMap } from '../properties/chemical-component';
-import { ComponentType, getMoleculeType, MoleculeType } from '../types';
-
-import mmCIF_Format = Format.mmCIF
+import { Model } from 'mol-model/structure/model/model';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { CustomProperties } from 'mol-model/structure/model/properties/custom';
+import { ModelSymmetry } from 'mol-model/structure/model/properties/symmetry';
+import { createAssemblies } from './assembly';
+import { getAtomicHierarchyAndConformation } from './atomic';
+import { ComponentBond } from './bonds';
+import { getIHMCoarse, EmptyIHMCoarse, IHMData } from './ihm';
+import { getSecondaryStructure } from './secondary-structure';
+import { getSequence } from './sequence';
+import { sortAtomSite } from './sort';
+import { StructConn } from './bonds/struct_conn';
+import { ChemicalComponent } from 'mol-model/structure/model/properties/chemical-component';
+import { getMoleculeType, MoleculeType } from 'mol-model/structure/model/types';
+import { ModelFormat } from '../format';
 import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from 'mol-model/structure/structure/carbohydrates/constants';
+import mmCIF_Format = ModelFormat.mmCIF
+import { memoize1 } from 'mol-util/memoize';
+
+export async function _parse_mmCif(format: mmCIF_Format, ctx: RuntimeContext) {
+    const formatData = getFormatData(format)
+    const isIHM = format.data.ihm_model_list._rowCount > 0;
+    return isIHM ? await readIHM(ctx, format, formatData) : await readStandard(ctx, format, formatData);
+}
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -69,10 +75,12 @@ function getNcsOperators(format: mmCIF_Format) {
         const m = Tensor.toMat3(matrixSpace, matrix.value(i));
         const v = Tensor.toVec3(vectorSpace, vector.value(i));
         if (!SymmetryOperator.checkIfRotationAndTranslation(m, v)) continue;
-        opers[opers.length] = SymmetryOperator.ofRotationAndOffset(`ncs_${id.value(i)}`, m, v);
+        const ncsId = id.value(i)
+        opers[opers.length] = SymmetryOperator.ofRotationAndOffset(`ncs_${ncsId}`, m, v, ncsId);
     }
     return opers;
 }
+
 function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['modifiedResidues'] {
     const data = format.data.pdbx_struct_mod_residue;
     const parentId = new Map<string, string>();
@@ -89,22 +97,14 @@ function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['m
     return { parentId, details };
 }
 
-function getChemicalComponentMap(format: mmCIF_Format): ChemicalComponentMap {
+function getChemicalComponentMap(format: mmCIF_Format): Model['properties']['chemicalComponentMap'] {
     const map = new Map<string, ChemicalComponent>();
-    const { id, type, name, pdbx_synonyms, formula, formula_weight } = format.data.chem_comp
-    for (let i = 0, il = id.rowCount; i < il; ++i) {
-        const _id = id.value(i)
-        const _type = type.value(i)
-        const cc: ChemicalComponent = {
-            id: _id,
-            type: ComponentType[_type],
-            moleculeType: getMoleculeType(_type, _id),
-            name: name.value(i),
-            synonyms: pdbx_synonyms.value(i),
-            formula: formula.value(i),
-            formulaWeight: formula_weight.value(i),
+    const { chem_comp } = format.data
+    if (chem_comp._rowCount > 0) {
+        const { id } = format.data.chem_comp
+        for (let i = 0, il = id.rowCount; i < il; ++i) {
+            map.set(id.value(i), Table.getRow(format.data.chem_comp, i))
         }
-        map.set(_id, cc)
     }
     return map
 }
@@ -137,12 +137,24 @@ function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap
             }
         }
     } else {
-        // TODO check if present in format.data.atom_site.label_comp_id
-        SaccharideCompIdMap.forEach((v, k) => map.set(k, v))
+        const uniqueNames = getUniqueComponentNames(format)
+        SaccharideCompIdMap.forEach((v, k) => {
+            if (uniqueNames.has(k)) map.set(k, v)
+        })
     }
     return map
 }
 
+const getUniqueComponentNames = memoize1((format: mmCIF_Format) => {
+    const uniqueNames = new Set<string>()
+    const data = format.data.atom_site
+    const comp_id = data.label_comp_id.isDefined ? data.label_comp_id : data.auth_comp_id;
+    for (let i = 0, il = comp_id.rowCount; i < il; ++i) {
+        uniqueNames.add(comp_id.value(i))
+    }
+    return uniqueNames
+})
+
 export interface FormatData {
     modifiedResidues: Model['properties']['modifiedResidues']
     chemicalComponentMap: Model['properties']['chemicalComponentMap']
@@ -157,8 +169,8 @@ function getFormatData(format: mmCIF_Format): FormatData {
     }
 }
 
-function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, formatData: FormatData, previous?: Model): Model {
-    const atomic = getAtomicHierarchyAndConformation(format, atom_site, entities, formatData, previous);
+function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, sourceIndex: Column<number>, entities: Entities, formatData: FormatData, previous?: Model): Model {
+    const atomic = getAtomicHierarchyAndConformation(atom_site, sourceIndex, entities, formatData, previous);
     if (previous && atomic.sameAsPrevious) {
         return {
             ...previous,
@@ -187,7 +199,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
         coarseHierarchy: coarse.hierarchy,
         coarseConformation: coarse.conformation,
         properties: {
-            secondaryStructure: getSecondaryStructureMmCif(format.data, atomic.hierarchy),
+            secondaryStructure: getSecondaryStructure(format.data, atomic.hierarchy, atomic.conformation),
             ...formatData
         },
         customProperties: new CustomProperties(),
@@ -197,7 +209,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
 }
 
 function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatData): Model {
-    const atomic = getAtomicHierarchyAndConformation(format, data.atom_site, data.entities, formatData);
+    const atomic = getAtomicHierarchyAndConformation(data.atom_site, data.atom_site_sourceIndex, data.entities, formatData);
     const coarse = getIHMCoarse(data, formatData);
 
     return {
@@ -213,7 +225,7 @@ function createModelIHM(format: mmCIF_Format, data: IHMData, formatData: FormatD
         coarseHierarchy: coarse.hierarchy,
         coarseConformation: coarse.conformation,
         properties: {
-            secondaryStructure: getSecondaryStructureMmCif(format.data, atomic.hierarchy),
+            secondaryStructure: getSecondaryStructure(format.data, atomic.hierarchy, atomic.conformation),
             ...formatData
         },
         customProperties: new CustomProperties(),
@@ -243,8 +255,8 @@ async function readStandard(ctx: RuntimeContext, format: mmCIF_Format, formatDat
     let modelStart = 0;
     while (modelStart < atomCount) {
         const modelEnd = findModelEnd(format.data.atom_site.pdbx_PDB_model_num, modelStart);
-        const atom_site = await sortAtomSite(ctx, format.data.atom_site, modelStart, modelEnd);
-        const model = createStandardModel(format, atom_site, entities, formatData, models.length > 0 ? models[models.length - 1] : void 0);
+        const { atom_site, sourceIndex } = await sortAtomSite(ctx, format.data.atom_site, modelStart, modelEnd);
+        const model = createStandardModel(format, atom_site, sourceIndex, entities, formatData, models.length > 0 ? models[models.length - 1] : void 0);
         attachProps(model);
         models.push(model);
         modelStart = modelEnd;
@@ -253,14 +265,17 @@ async function readStandard(ctx: RuntimeContext, format: mmCIF_Format, formatDat
 }
 
 function splitTable<T extends Table<any>>(table: T, col: Column<number>) {
-    const ret = new Map<number, T>()
+    const ret = new Map<number, { table: T, start: number, end: number }>()
     const rowCount = table._rowCount;
     let modelStart = 0;
     while (modelStart < rowCount) {
         const modelEnd = findModelEnd(col, modelStart);
         const id = col.value(modelStart);
-        const window = Table.window(table, table._schema, modelStart, modelEnd) as T;
-        ret.set(id, window);
+        ret.set(id, {
+            table: Table.window(table, table._schema, modelStart, modelEnd) as T,
+            start: modelStart,
+            end: modelEnd
+        });
         modelStart = modelEnd;
     }
     return ret;
@@ -270,8 +285,14 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
     const { ihm_model_list } = format.data;
     const entities: Entities = { data: format.data.entity, getEntityIndex: Column.createIndexer(format.data.entity.id) };
 
-    // TODO: will IHM require sorting or will we trust it?
+    if (format.data.atom_site._rowCount && !format.data.atom_site.ihm_model_id.isDefined) {
+        throw new Error('expected _atom_site.ihm_model_id to be defined')
+    }
+
     const atom_sites = splitTable(format.data.atom_site, format.data.atom_site.ihm_model_id);
+    // TODO: will coarse IHM records require sorting or will we trust it?
+    // ==> Probably implement a sort as as well and store the sourceIndex same as with atomSite
+    // If the sorting is implemented, updated mol-model/structure/properties: atom.sourceIndex
     const sphere_sites = splitTable(format.data.ihm_sphere_obj_site, format.data.ihm_sphere_obj_site.model_id);
     const gauss_sites = splitTable(format.data.ihm_gaussian_obj_site, format.data.ihm_gaussian_obj_site.model_id);
 
@@ -280,13 +301,26 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
     const { model_id, model_name } = ihm_model_list;
     for (let i = 0; i < ihm_model_list._rowCount; i++) {
         const id = model_id.value(i);
+
+        let atom_site, atom_site_sourceIndex;
+        if (atom_sites.has(id)) {
+            const e = atom_sites.get(id)!;
+            const { atom_site: sorted, sourceIndex } = await sortAtomSite(ctx, e.table, e.start, e.end);
+            atom_site = sorted;
+            atom_site_sourceIndex = sourceIndex;
+        } else {
+            atom_site = Table.window(format.data.atom_site, format.data.atom_site._schema, 0, 0);
+            atom_site_sourceIndex = Column.ofIntArray([]);
+        }
+
         const data: IHMData = {
             model_id: id,
             model_name: model_name.value(i),
             entities: entities,
-            atom_site: atom_sites.has(id) ? atom_sites.get(id)! : Table.window(format.data.atom_site, format.data.atom_site._schema, 0, 0),
-            ihm_sphere_obj_site: sphere_sites.has(id) ? sphere_sites.get(id)! : Table.window(format.data.ihm_sphere_obj_site, format.data.ihm_sphere_obj_site._schema, 0, 0),
-            ihm_gaussian_obj_site: gauss_sites.has(id) ? gauss_sites.get(id)! : Table.window(format.data.ihm_gaussian_obj_site, format.data.ihm_gaussian_obj_site._schema, 0, 0)
+            atom_site,
+            atom_site_sourceIndex,
+            ihm_sphere_obj_site: sphere_sites.has(id) ? sphere_sites.get(id)!.table : Table.window(format.data.ihm_sphere_obj_site, format.data.ihm_sphere_obj_site._schema, 0, 0),
+            ihm_gaussian_obj_site: gauss_sites.has(id) ? gauss_sites.get(id)!.table : Table.window(format.data.ihm_gaussian_obj_site, format.data.ihm_gaussian_obj_site._schema, 0, 0)
         };
         const model = createModelIHM(format, data, formatData);
         attachProps(model);
@@ -294,14 +328,4 @@ async function readIHM(ctx: RuntimeContext, format: mmCIF_Format, formatData: Fo
     }
 
     return models;
-}
-
-function buildModels(format: mmCIF_Format): Task<ReadonlyArray<Model>> {
-    const formatData = getFormatData(format)
-    return Task.create('Create mmCIF Model', async ctx => {
-        const isIHM = format.data.ihm_model_list._rowCount > 0;
-        return isIHM ? await readIHM(ctx, format, formatData) : await readStandard(ctx, format, formatData);
-    });
-}
-
-export default buildModels;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif/secondary-structure.ts b/src/mol-model-formats/structure/mmcif/secondary-structure.ts
similarity index 88%
rename from src/mol-model/structure/model/formats/mmcif/secondary-structure.ts
rename to src/mol-model-formats/structure/mmcif/secondary-structure.ts
index d871052c96890e86bdd7c0b241755a944d2e5b20..0ba80546f6aa6d92d472d0a541f4dd7aed16671b 100644
--- a/src/mol-model/structure/model/formats/mmcif/secondary-structure.ts
+++ b/src/mol-model-formats/structure/mmcif/secondary-structure.ts
@@ -6,11 +6,21 @@
  */
 
 import { mmCIF_Database as mmCIF, mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'
-import { SecondaryStructureType } from '../../types';
-import { AtomicHierarchy } from '../../properties/atomic';
-import { SecondaryStructure } from '../../properties/seconday-structure';
+import { SecondaryStructureType } from 'mol-model/structure/model/types';
+import { AtomicHierarchy, AtomicConformation } from 'mol-model/structure/model/properties/atomic';
+import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
 import { Column } from 'mol-data/db';
-import { ChainIndex, ResidueIndex } from '../../indexing';
+import { ChainIndex, ResidueIndex } from 'mol-model/structure/model/indexing';
+import { computeSecondaryStructure } from 'mol-model/structure/model/properties/utils/secondary-structure';
+
+// TODO add parameter to allow forcing computation
+export function getSecondaryStructure(data: mmCIF_Database, hierarchy: AtomicHierarchy, conformation: AtomicConformation): SecondaryStructure {
+    if (!data.struct_conf._rowCount && !data.struct_sheet_range._rowCount) {
+        return computeSecondaryStructure(hierarchy, conformation)
+    } else {
+        return getSecondaryStructureMmCif(data, hierarchy)
+    }
+}
 
 export function getSecondaryStructureMmCif(data: mmCIF_Database, hierarchy: AtomicHierarchy): SecondaryStructure {
     const map: SecondaryStructureMap = new Map();
diff --git a/src/mol-model/structure/model/formats/mmcif/sequence.ts b/src/mol-model-formats/structure/mmcif/sequence.ts
similarity index 87%
rename from src/mol-model/structure/model/formats/mmcif/sequence.ts
rename to src/mol-model-formats/structure/mmcif/sequence.ts
index 70caf352fd19e0ffa1177dc0615c90b825f72417..9915651b634980c5c522935caa45eabc3c702aee 100644
--- a/src/mol-model/structure/model/formats/mmcif/sequence.ts
+++ b/src/mol-model-formats/structure/mmcif/sequence.ts
@@ -5,11 +5,11 @@
  */
 
 import { mmCIF_Database as mmCIF } from 'mol-io/reader/cif/schema/mmcif'
-import StructureSequence from '../../properties/sequence'
+import StructureSequence from 'mol-model/structure/model/properties/sequence'
 import { Column } from 'mol-data/db';
-import { AtomicHierarchy } from '../../properties/atomic';
-import { Entities } from '../../properties/common';
-import { Sequence } from '../../../../sequence';
+import { AtomicHierarchy } from 'mol-model/structure/model/properties/atomic';
+import { Entities } from 'mol-model/structure/model/properties/common';
+import { Sequence } from 'mol-model/sequence';
 
 // TODO how to handle microheterogeneity
 //    see http://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v50.dic/Categories/entity_poly_seq.html
diff --git a/src/mol-model/structure/model/formats/mmcif/sort.ts b/src/mol-model-formats/structure/mmcif/sort.ts
similarity index 83%
rename from src/mol-model/structure/model/formats/mmcif/sort.ts
rename to src/mol-model-formats/structure/mmcif/sort.ts
index 7cbee9e5b102854bb2c80120fbc4fbfe2baa57bf..868cd5dd86bed2dfb063a042a224fb54191ed52b 100644
--- a/src/mol-model/structure/model/formats/mmcif/sort.ts
+++ b/src/mol-model-formats/structure/mmcif/sort.ts
@@ -9,6 +9,8 @@ import { createRangeArray, makeBuckets } from 'mol-data/util';
 import { Column, Table } from 'mol-data/db';
 import { RuntimeContext } from 'mol-task';
 
+export type SortedAtomSite = mmCIF_Database['atom_site'] & { sourceIndex: Column<number> }
+
 function isIdentity(xs: ArrayLike<number>) {
     for (let i = 0, _i = xs.length; i < _i; i++) {
         if (xs[i] !== i) return false;
@@ -36,8 +38,11 @@ export async function sortAtomSite(ctx: RuntimeContext, atom_site: mmCIF_Databas
     }
 
     if (isIdentity(indices) && indices.length === atom_site._rowCount) {
-        return atom_site;
+        return { atom_site, sourceIndex: Column.ofIntArray(indices) };
     }
 
-    return Table.view(atom_site, atom_site._schema, indices) as mmCIF_Database['atom_site'];
+    return {
+        atom_site: Table.view(atom_site, atom_site._schema, indices) as mmCIF_Database['atom_site'],
+        sourceIndex: Column.ofIntArray(indices)
+    };
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif/util.ts b/src/mol-model-formats/structure/mmcif/util.ts
similarity index 89%
rename from src/mol-model/structure/model/formats/mmcif/util.ts
rename to src/mol-model-formats/structure/mmcif/util.ts
index 044dee3a6bc4aba02a65e9d047ec0c3e7afe2188..1f398a11bee1cfd425447854ecc9e0bc36bfe71a 100644
--- a/src/mol-model/structure/model/formats/mmcif/util.ts
+++ b/src/mol-model-formats/structure/mmcif/util.ts
@@ -4,8 +4,8 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Model } from '../../model'
-import { ElementIndex } from '../../indexing';
+import { Model } from 'mol-model/structure/model'
+import { ElementIndex } from 'mol-model/structure/model/indexing';
 
 export function findEntityIdByAsymId(model: Model, asymId: string) {
     if (model.sourceData.kind !== 'mmCIF') return ''
diff --git a/src/mol-model-formats/structure/pdb.ts b/src/mol-model-formats/structure/pdb.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cce4dc869705609ee7cd5a17edce686313547097
--- /dev/null
+++ b/src/mol-model-formats/structure/pdb.ts
@@ -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 { PdbFile } from 'mol-io/reader/pdb/schema';
+import { pdbToMmCif } from './pdb/to-cif';
+import { Model } from 'mol-model/structure/model';
+import { Task } from 'mol-task';
+import { ModelFormat } from './format';
+import { _parse_mmCif } from './mmcif/parser';
+
+export function trajectoryFromPDB(pdb: PdbFile): Task<Model.Trajectory> {
+    return Task.create('Parse PDB', async ctx => {
+        await ctx.update('Converting to mmCIF');
+        const cif = await pdbToMmCif(pdb);
+        const format = ModelFormat.mmCIF(cif);
+        return _parse_mmCif(format, ctx);
+    })
+}
diff --git a/src/mol-model-formats/structure/pdb/assembly.ts b/src/mol-model-formats/structure/pdb/assembly.ts
new file mode 100644
index 0000000000000000000000000000000000000000..20ae0fdc51476f66d0f1c0cd7503651b79299047
--- /dev/null
+++ b/src/mol-model-formats/structure/pdb/assembly.ts
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CifCategory, CifField } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { Tokens } from 'mol-io/reader/common/text/tokenizer';
+
+export function parseCryst1(id: string, record: string): CifCategory[] {
+    // COLUMNS       DATA TYPE      CONTENTS
+    // --------------------------------------------------------------------------------
+    //  1 -  6       Record name    "CRYST1"
+    //  7 - 15       Real(9.3)      a (Angstroms)
+    // 16 - 24       Real(9.3)      b (Angstroms)
+    // 25 - 33       Real(9.3)      c (Angstroms)
+    // 34 - 40       Real(7.2)      alpha (degrees)
+    // 41 - 47       Real(7.2)      beta (degrees)
+    // 48 - 54       Real(7.2)      gamma (degrees)
+    // 56 - 66       LString        Space group
+    // 67 - 70       Integer        Z value
+
+    const get = (s: number, l: number) => (record.substr(s, l) || '').trim()
+
+    const cell: CifCategory.Fields<mmCIF_Schema['cell']> = {
+        entry_id: CifField.ofString(id),
+        length_a: CifField.ofString(get(6, 9)),
+        length_b: CifField.ofString(get(15, 9)),
+        length_c: CifField.ofString(get(24, 9)),
+        angle_alpha: CifField.ofString(get(33, 7)),
+        angle_beta: CifField.ofString(get(40, 7)),
+        angle_gamma: CifField.ofString(get(47, 7)),
+        Z_PDB: CifField.ofString(get(66, 4)),
+        pdbx_unique_axis: CifField.ofString('?')
+    };
+    const symmetry: CifCategory.Fields<mmCIF_Schema['symmetry']> = {
+        entry_id: CifField.ofString(id),
+        'space_group_name_H-M': CifField.ofString(get(55, 11)),
+        Int_Tables_number: CifField.ofString('?'),
+        cell_setting: CifField.ofString('?'),
+        space_group_name_Hall: CifField.ofString('?')
+    }
+    return [CifCategory.ofFields('cell', cell), CifCategory.ofFields('symmetry', symmetry)];
+}
+
+interface PdbAssembly {
+    id: string,
+    details: string,
+    groups: { chains: string[], operators: { id: number, matrix: Mat4 }[] }[]
+}
+
+function PdbAssembly(id: string, details: string): PdbAssembly {
+    return { id, details, groups: [] };
+}
+
+export function parseRemark350(lines: Tokens, lineStart: number, lineEnd: number): CifCategory[] {
+    const assemblies: PdbAssembly[] = [];
+
+    // Read the assemblies
+    let current: PdbAssembly, group: PdbAssembly['groups'][0], matrix: Mat4, operId = 1;
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        if (line.substr(11, 12) === 'BIOMOLECULE:') {
+            const id = line.substr(23).trim();
+            let details: string = `Biomolecule ` + id;
+            line = getLine(i + 1);
+            if (line.substr(11, 30) !== 'APPLY THE FOLLOWING TO CHAINS:') {
+                i++;
+                details = line.substr(11).trim();
+            }
+            current = PdbAssembly(id, details);
+            assemblies.push(current);
+        } else if (line.substr(13, 5) === 'BIOMT') {
+            const biomt = line.split(/\s+/)
+            const row = parseInt(line[18]) - 1
+
+            if (row === 0) {
+                matrix = Mat4.identity();
+                group!.operators.push({ id: operId++, matrix });
+            }
+
+            Mat4.setValue(matrix!, row, 0, parseFloat(biomt[4]));
+            Mat4.setValue(matrix!, row, 1, parseFloat(biomt[5]));
+            Mat4.setValue(matrix!, row, 2, parseFloat(biomt[6]));
+            Mat4.setValue(matrix!, row, 3, parseFloat(biomt[7]));
+        } else if (
+            line.substr(11, 30) === 'APPLY THE FOLLOWING TO CHAINS:' ||
+            line.substr(11, 30) === '                   AND CHAINS:') {
+
+            if (line.substr(11, 5) === 'APPLY') {
+                group = { chains: [], operators: [] };
+                current!.groups.push(group);
+            }
+
+            const chainList = line.substr(41, 30).split(',');
+            for (let j = 0, jl = chainList.length; j < jl; ++j) {
+                const c = chainList[j].trim();
+                if (c) group!.chains.push(c);
+            }
+        }
+    }
+
+    if (assemblies.length === 0) return [];
+
+    // Generate CIF
+
+    // pdbx_struct_assembly
+    const pdbx_struct_assembly: CifCategory.SomeFields<mmCIF_Schema['pdbx_struct_assembly']> = {
+        id: CifField.ofStrings(assemblies.map(a => a.id)),
+        details: CifField.ofStrings(assemblies.map(a => a.details))
+    };
+
+
+    // pdbx_struct_assembly_gen
+    const pdbx_struct_assembly_gen_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['pdbx_struct_assembly_gen']>]: string }[] = [];
+    for (const asm of assemblies) {
+        for (const group of asm.groups) {
+            pdbx_struct_assembly_gen_rows.push({
+                assembly_id: asm.id,
+                oper_expression: group.operators.map(o => o.id).join(','),
+                asym_id_list: group.chains.join(',')
+            });
+        }
+    }
+    const pdbx_struct_assembly_gen: CifCategory.Fields<mmCIF_Schema['pdbx_struct_assembly_gen']> = {
+        assembly_id: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.assembly_id)),
+        oper_expression: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.oper_expression)),
+        asym_id_list: CifField.ofStrings(pdbx_struct_assembly_gen_rows.map(r => r.asym_id_list))
+    };
+
+    // pdbx_struct_oper_list
+    const pdbx_struct_oper_list_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['pdbx_struct_oper_list']>]?: string }[] = [];
+    for (const asm of assemblies) {
+        for (const group of asm.groups) {
+            for (const oper of group.operators) {
+                const row = {
+                    id: '' + oper.id,
+                    type: '?',
+                    name: '?',
+                    symmetry_operation: '?'
+                } as (typeof pdbx_struct_oper_list_rows)[0] as any;
+                for (let i = 0; i < 3; i++) {
+                    for (let j = 0; j < 3; j++) {
+                        row[`matrix[${i + 1}][${j + 1}]`] = '' + Mat4.getValue(oper.matrix, i, j);
+                    }
+                    row[`vector[${i + 1}]`] = '' + Mat4.getValue(oper.matrix, i, 3);
+                }
+                pdbx_struct_oper_list_rows.push(row);
+            }
+        }
+    }
+
+    const pdbx_struct_oper_list: CifCategory.SomeFields<mmCIF_Schema['pdbx_struct_oper_list']> = {
+        id: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.id!)),
+        type: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.type!)),
+        name: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.name!)),
+        symmetry_operation: CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => r.symmetry_operation!))
+    };
+    for (let i = 0; i < 3; i++) {
+        for (let j = 0; j < 3; j++) {
+            const k = `matrix[${i + 1}][${j + 1}]`;
+            (pdbx_struct_oper_list as any)[k] = CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => (r as any)[k]!));
+        }
+        const k = `vector[${i + 1}]`;
+        (pdbx_struct_oper_list as any)[k] = CifField.ofStrings(pdbx_struct_oper_list_rows.map(r => (r as any)[k]!));
+    }
+
+    return [
+        CifCategory.ofFields('pdbx_struct_assembly', pdbx_struct_assembly),
+        CifCategory.ofFields('pdbx_struct_assembly_gen', pdbx_struct_assembly_gen),
+        CifCategory.ofFields('pdbx_struct_oper_list', pdbx_struct_oper_list)
+    ];
+}
+
+export function parseMtrix(lines: Tokens, lineStart: number, lineEnd: number): CifCategory[] {
+    const matrices: Mat4[] = [];
+    let matrix: Mat4;
+
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+
+        const ncs = line.split(/\s+/);
+        const row = parseInt(line[5]) - 1;
+
+        if (row === 0) {
+            matrix = Mat4.identity();
+            matrices.push(matrix);
+        }
+
+        Mat4.setValue(matrix!, row, 0, parseFloat(ncs[2]));
+        Mat4.setValue(matrix!, row, 1, parseFloat(ncs[3]));
+        Mat4.setValue(matrix!, row, 2, parseFloat(ncs[4]));
+        Mat4.setValue(matrix!, row, 3, parseFloat(ncs[5]));
+    }
+
+    if (matrices.length === 0) return [];
+
+    const struct_ncs_oper_rows: { [P in keyof CifCategory.Fields<mmCIF_Schema['struct_ncs_oper']>]?: string }[] = [];
+    let id = 1;
+    for (const oper of matrices) {
+            const row = {
+                id: 'ncsop' + (id++),
+                code: '.',
+                details: '.'
+            } as (typeof struct_ncs_oper_rows)[0] as any;
+            for (let i = 0; i < 3; i++) {
+                for (let j = 0; j < 3; j++) {
+                    row[`matrix[${i + 1}][${j + 1}]`] = '' + Mat4.getValue(oper, i, j);
+                }
+                row[`vector[${i + 1}]`] = '' + Mat4.getValue(oper, i, 3);
+            }
+            struct_ncs_oper_rows.push(row);
+    }
+
+    const struct_ncs_oper: CifCategory.SomeFields<mmCIF_Schema['struct_ncs_oper']> = {
+        id: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.id!)),
+        code: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.code!)),
+        details: CifField.ofStrings(struct_ncs_oper_rows.map(r => r.details!)),
+    };
+    for (let i = 0; i < 3; i++) {
+        for (let j = 0; j < 3; j++) {
+            const k = `matrix[${i + 1}][${j + 1}]`;
+            (struct_ncs_oper as any)[k] = CifField.ofStrings(struct_ncs_oper_rows.map(r => (r as any)[k]!));
+        }
+        const k = `vector[${i + 1}]`;
+        (struct_ncs_oper as any)[k] = CifField.ofStrings(struct_ncs_oper_rows.map(r => (r as any)[k]!));
+    }
+
+    return [CifCategory.ofFields('struct_ncs_oper', struct_ncs_oper)];
+}
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/pdb/secondary-structure.ts b/src/mol-model-formats/structure/pdb/secondary-structure.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d5b92c64a2075c694037ee5a4ea38f7f4d9d82b3
--- /dev/null
+++ b/src/mol-model-formats/structure/pdb/secondary-structure.ts
@@ -0,0 +1,262 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { CifCategory, CifField } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Tokens } from 'mol-io/reader/common/text/tokenizer';
+
+const HelixTypes: {[k: string]: mmCIF_Schema['struct_conf']['conf_type_id']['T']} = {
+    // CLASS NUMBER
+    // TYPE OF  HELIX                     (COLUMNS 39 - 40)
+    // --------------------------------------------------------------
+    // Right-handed alpha (default)                1
+    // Right-handed omega                          2
+    // Right-handed pi                             3
+    // Right-handed gamma                          4
+    // Right-handed 3 - 10                         5
+    // Left-handed alpha                           6
+    // Left-handed omega                           7
+    // Left-handed gamma                           8
+    // 2 - 7 ribbon/helix                          9
+    // Polyproline                                10
+    1: 'HELX_RH_AL_P',
+    2: 'HELX_RH_OM_P',
+    3: 'HELX_RH_PI_P',
+    4: 'HELX_RH_GA_P',
+    5: 'HELX_RH_3T_P',
+    6: 'HELX_LH_AL_P',
+    7: 'HELX_LH_OM_P',
+    8: 'HELX_LH_GA_P',
+    9: 'HELX_RH_27_P', // TODO or left-handed???
+    10: 'HELX_RH_PP_P', // TODO or left-handed???
+}
+function getStructConfTypeId(type: string): mmCIF_Schema['struct_conf']['conf_type_id']['T'] {
+    return HelixTypes[type] || 'HELX_P'
+}
+
+interface PdbHelix {
+    serNum: string,
+    helixID: string,
+    initResName: string,
+    initChainID: string,
+    initSeqNum: string,
+    initICode: string,
+    endResName: string,
+    endChainID: string,
+    endSeqNum: string,
+    endICode: string,
+    helixClass: string,
+    comment: string,
+    length: string
+}
+
+export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): CifCategory {
+    const helices: PdbHelix[] = []
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        // COLUMNS        DATA  TYPE     FIELD         DEFINITION
+        // -----------------------------------------------------------------------------------
+        // 1 -  6        Record name    "HELIX "
+        // 8 - 10        Integer        serNum        Serial number of the helix. This starts
+        //                                             at 1  and increases incrementally.
+        // 12 - 14        LString(3)     helixID       Helix  identifier. In addition to a serial
+        //                                             number, each helix is given an
+        //                                             alphanumeric character helix identifier.
+        // 16 - 18        Residue name   initResName   Name of the initial residue.
+        // 20             Character      initChainID   Chain identifier for the chain containing
+        //                                             this  helix.
+        // 22 - 25        Integer        initSeqNum    Sequence number of the initial residue.
+        // 26             AChar          initICode     Insertion code of the initial residue.
+        // 28 - 30        Residue  name  endResName    Name of the terminal residue of the helix.
+        // 32             Character      endChainID    Chain identifier for the chain containing
+        //                                             this  helix.
+        // 34 - 37        Integer        endSeqNum     Sequence number of the terminal residue.
+        // 38             AChar          endICode      Insertion code of the terminal residue.
+        // 39 - 40        Integer        helixClass    Helix class (see below).
+        // 41 - 70        String         comment       Comment about this helix.
+        // 72 - 76        Integer        length        Length of this helix.
+        helices.push({
+            serNum: line.substr(7, 3).trim(),
+            helixID: line.substr(11, 3).trim(),
+            initResName: line.substr(15, 3).trim(),
+            initChainID: line.substr(19, 1).trim(),
+            initSeqNum: line.substr(21, 4).trim(),
+            initICode: line.substr(25, 1).trim(),
+            endResName: line.substr(27, 3).trim(),
+            endChainID: line.substr(31, 3).trim(),
+            endSeqNum: line.substr(33, 4).trim(),
+            endICode: line.substr(37, 1).trim(),
+            helixClass: line.substr(38, 2).trim(),
+            comment: line.substr(40, 30).trim(),
+            length: line.substr(71, 5).trim()
+        })
+    }
+
+    const beg_auth_asym_id = CifField.ofStrings(helices.map(h => h.initChainID))
+    const beg_auth_comp_id = CifField.ofStrings(helices.map(h => h.initResName))
+    const beg_auth_seq_id = CifField.ofStrings(helices.map(h => h.initSeqNum))
+
+    const end_auth_asym_id = CifField.ofStrings(helices.map(h => h.endChainID))
+    const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName))
+    const end_auth_seq_id = CifField.ofStrings(helices.map(h => h.endSeqNum))
+
+    const struct_conf: CifCategory.Fields<mmCIF_Schema['struct_conf']> = {
+        beg_label_asym_id: beg_auth_asym_id,
+        beg_label_comp_id: beg_auth_comp_id,
+        beg_label_seq_id: beg_auth_seq_id,
+        beg_auth_asym_id,
+        beg_auth_comp_id,
+        beg_auth_seq_id,
+
+        conf_type_id: CifField.ofStrings(helices.map(h => getStructConfTypeId(h.helixClass))),
+        details: CifField.ofStrings(helices.map(h => h.comment)),
+
+        end_label_asym_id: end_auth_asym_id,
+        end_label_comp_id: end_auth_asym_id,
+        end_label_seq_id: end_auth_seq_id,
+        end_auth_asym_id,
+        end_auth_comp_id,
+        end_auth_seq_id,
+
+        id: CifField.ofStrings(helices.map(h => h.serNum)),
+        pdbx_beg_PDB_ins_code: CifField.ofStrings(helices.map(h => h.initICode)),
+        pdbx_end_PDB_ins_code: CifField.ofStrings(helices.map(h => h.endICode)),
+        pdbx_PDB_helix_class: CifField.ofStrings(helices.map(h => h.helixClass)),
+        pdbx_PDB_helix_length: CifField.ofStrings(helices.map(h => h.length)),
+        pdbx_PDB_helix_id: CifField.ofStrings(helices.map(h => h.helixID)),
+    };
+    return CifCategory.ofFields('struct_conf', struct_conf);
+}
+
+//
+
+interface PdbSheet {
+    strand: string,
+    sheetID: string,
+    numStrands: string,
+    initResName: string,
+    initChainID: string,
+    initSeqNum: string,
+    initICode: string,
+    endResName: string,
+    endChainID: string,
+    endSeqNum: string,
+    endICode: string,
+    sense: string,
+    curAtom: string,
+    curResName: string,
+    curChainId: string,
+    curResSeq: string,
+    curICode: string,
+    prevAtom: string,
+    prevResName: string,
+    prevChainId: string,
+    prevResSeq: string,
+    prevICode: string,
+}
+
+export function parseSheet(lines: Tokens, lineStart: number, lineEnd: number): CifCategory {
+    const sheets: PdbSheet[] = []
+    const getLine = (n: number) => lines.data.substring(lines.indices[2 * n], lines.indices[2 * n + 1]);
+
+    for (let i = lineStart; i < lineEnd; i++) {
+        let line = getLine(i);
+        // COLUMNS       DATA  TYPE     FIELD          DEFINITION
+        // -------------------------------------------------------------------------------------
+        // 1 -  6        Record name   "SHEET "
+        // 8 - 10        Integer       strand         Strand  number which starts at 1 for each
+        //                                             strand within a sheet and increases by one.
+        // 12 - 14        LString(3)    sheetID        Sheet  identifier.
+        // 15 - 16        Integer       numStrands     Number  of strands in sheet.
+        // 18 - 20        Residue name  initResName    Residue  name of initial residue.
+        // 22             Character     initChainID    Chain identifier of initial residue
+        //                                             in strand.
+        // 23 - 26        Integer       initSeqNum     Sequence number of initial residue
+        //                                             in strand.
+        // 27             AChar         initICode      Insertion code of initial residue
+        //                                             in  strand.
+        // 29 - 31        Residue name  endResName     Residue name of terminal residue.
+        // 33             Character     endChainID     Chain identifier of terminal residue.
+        // 34 - 37        Integer       endSeqNum      Sequence number of terminal residue.
+        // 38             AChar         endICode       Insertion code of terminal residue.
+        // 39 - 40        Integer       sense          Sense of strand with respect to previous
+        //                                             strand in the sheet. 0 if first strand,
+        //                                             1 if  parallel,and -1 if anti-parallel.
+        // 42 - 45        Atom          curAtom        Registration.  Atom name in current strand.
+        // 46 - 48        Residue name  curResName     Registration.  Residue name in current strand
+        // 50             Character     curChainId     Registration. Chain identifier in
+        //                                             current strand.
+        // 51 - 54        Integer       curResSeq      Registration.  Residue sequence number
+        //                                             in current strand.
+        // 55             AChar         curICode       Registration. Insertion code in
+        //                                             current strand.
+        // 57 - 60        Atom          prevAtom       Registration.  Atom name in previous strand.
+        // 61 - 63        Residue name  prevResName    Registration.  Residue name in
+        //                                             previous strand.
+        // 65             Character     prevChainId    Registration.  Chain identifier in
+        //                                             previous  strand.
+        // 66 - 69        Integer       prevResSeq     Registration. Residue sequence number
+        //                                             in previous strand.
+        // 70             AChar         prevICode      Registration.  Insertion code in
+        //                                             previous strand.
+        sheets.push({
+            strand: line.substr(7, 3).trim(),
+            sheetID: line.substr(11, 3).trim(),
+            numStrands: line.substr(14, 2).trim(),
+            initResName: line.substr(17, 3).trim(),
+            initChainID: line.substr(21, 1).trim(),
+            initSeqNum: line.substr(22, 4).trim(),
+            initICode: line.substr(26, 1).trim(),
+            endResName: line.substr(28, 3).trim(),
+            endChainID: line.substr(32, 1).trim(),
+            endSeqNum: line.substr(33, 4).trim(),
+            endICode: line.substr(37, 1).trim(),
+            sense: line.substr(38, 2).trim(),
+            curAtom: line.substr(41, 4).trim(),
+            curResName: line.substr(45, 3).trim(),
+            curChainId: line.substr(49, 1).trim(),
+            curResSeq: line.substr(50, 4).trim(),
+            curICode: line.substr(54, 1).trim(),
+            prevAtom: line.substr(56, 4).trim(),
+            prevResName: line.substr(60, 3).trim(),
+            prevChainId: line.substr(64, 1).trim(),
+            prevResSeq: line.substr(65, 4).trim(),
+            prevICode: line.substr(69, 1).trim(),
+        })
+    }
+
+    const beg_auth_asym_id = CifField.ofStrings(sheets.map(s => s.initChainID))
+    const beg_auth_comp_id = CifField.ofStrings(sheets.map(s => s.initResName))
+    const beg_auth_seq_id = CifField.ofStrings(sheets.map(s => s.initSeqNum))
+
+    const end_auth_asym_id = CifField.ofStrings(sheets.map(s => s.endChainID))
+    const end_auth_comp_id = CifField.ofStrings(sheets.map(s => s.endResName))
+    const end_auth_seq_id = CifField.ofStrings(sheets.map(s => s.endSeqNum))
+
+    const struct_sheet_range: CifCategory.Fields<mmCIF_Schema['struct_sheet_range']> = {
+        beg_label_asym_id: beg_auth_asym_id,
+        beg_label_comp_id: beg_auth_comp_id,
+        beg_label_seq_id: beg_auth_seq_id,
+        beg_auth_asym_id,
+        beg_auth_comp_id,
+        beg_auth_seq_id,
+
+        end_label_asym_id: end_auth_asym_id,
+        end_label_comp_id: end_auth_asym_id,
+        end_label_seq_id: end_auth_seq_id,
+        end_auth_asym_id,
+        end_auth_comp_id,
+        end_auth_seq_id,
+
+        id: CifField.ofStrings(sheets.map(s => s.strand)),
+        sheet_id: CifField.ofStrings(sheets.map(s => s.sheetID)), // TODO wrong, needs to point to _struct_sheet.id
+        pdbx_beg_PDB_ins_code: CifField.ofStrings(sheets.map(s => s.initICode)),
+        pdbx_end_PDB_ins_code: CifField.ofStrings(sheets.map(s => s.endICode)),
+    };
+    return CifCategory.ofFields('struct_sheet_range', struct_sheet_range);
+}
\ No newline at end of file
diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts
new file mode 100644
index 0000000000000000000000000000000000000000..853a1b9319eb97121a5394a55e772dde2621608a
--- /dev/null
+++ b/src/mol-model-formats/structure/pdb/to-cif.ts
@@ -0,0 +1,296 @@
+/**
+ * Copyright (c) 2019 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 { substringStartsWith } from 'mol-util/string';
+import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer';
+import { PdbFile } from 'mol-io/reader/pdb/schema';
+import { parseCryst1, parseRemark350, parseMtrix } from './assembly';
+import { WaterNames } from 'mol-model/structure/model/types';
+import { parseHelix, parseSheet } from './secondary-structure';
+
+function _entity(): { [K in keyof mmCIF_Schema['entity']]?: CifField } {
+    return {
+        id: CifField.ofStrings(['1', '2', '3']),
+        type: CifField.ofStrings(['polymer', 'non-polymer', 'water'])
+    }
+}
+
+type AtomSiteTemplate = typeof atom_site_template extends (...args: any) => infer T ? T : never
+function atom_site_template(data: string, count: number) {
+    const str = () => [] as string[];
+    const ts = () => TokenBuilder.create(data, 2 * count);
+    return {
+        index: 0,
+        count,
+        group_PDB: ts(),
+        id: str(),
+        auth_atom_id: ts(),
+        label_alt_id: ts(),
+        auth_comp_id: ts(),
+        auth_asym_id: ts(),
+        auth_seq_id: ts(),
+        pdbx_PDB_ins_code: ts(),
+        Cartn_x: ts(),
+        Cartn_y: ts(),
+        Cartn_z: ts(),
+        occupancy: ts(),
+        B_iso_or_equiv: ts(),
+        type_symbol: ts(),
+        pdbx_PDB_model_num: str(),
+        label_entity_id: str()
+    };
+}
+
+function _atom_site(sites: AtomSiteTemplate): { [K in keyof mmCIF_Schema['atom_site']]?: CifField } {
+    const auth_asym_id = CifField.ofTokens(sites.auth_asym_id);
+    const auth_atom_id = CifField.ofTokens(sites.auth_atom_id);
+    const auth_comp_id = CifField.ofTokens(sites.auth_comp_id);
+    const auth_seq_id = CifField.ofTokens(sites.auth_seq_id);
+
+    return {
+        auth_asym_id,
+        auth_atom_id,
+        auth_comp_id,
+        auth_seq_id,
+        B_iso_or_equiv: CifField.ofTokens(sites.B_iso_or_equiv),
+        Cartn_x: CifField.ofTokens(sites.Cartn_x),
+        Cartn_y: CifField.ofTokens(sites.Cartn_y),
+        Cartn_z: CifField.ofTokens(sites.Cartn_z),
+        group_PDB: CifField.ofTokens(sites.group_PDB),
+        id: CifField.ofStrings(sites.id),
+
+        label_alt_id: CifField.ofTokens(sites.label_alt_id),
+
+        label_asym_id: auth_asym_id,
+        label_atom_id: auth_atom_id,
+        label_comp_id: auth_comp_id,
+        label_seq_id: auth_seq_id,
+        label_entity_id: CifField.ofStrings(sites.label_entity_id),
+
+        occupancy: CifField.ofTokens(sites.occupancy),
+        type_symbol: CifField.ofTokens(sites.type_symbol),
+
+        pdbx_PDB_ins_code: CifField.ofTokens(sites.pdbx_PDB_ins_code),
+        pdbx_PDB_model_num: CifField.ofStrings(sites.pdbx_PDB_model_num)
+    };
+}
+
+function getEntityId(residueName: string, isHet: boolean) {
+    if (isHet) {
+        if (WaterNames.has(residueName)) return '3';
+        return '2';
+    }
+    return '1';
+}
+
+function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) {
+    const { data: str } = data;
+    const length = e - s;
+
+    // TODO: filter invalid atoms
+
+    // COLUMNS        DATA TYPE       CONTENTS
+    // --------------------------------------------------------------------------------
+    // 1 -  6        Record name     "ATOM  "
+    TokenBuilder.addToken(sites.group_PDB, Tokenizer.trim(data, s, s + 6));
+
+    // 7 - 11        Integer         Atom serial number.
+    // TODO: support HEX
+    Tokenizer.trim(data, s + 6, s + 11);
+    sites.id[sites.index] = data.data.substring(data.tokenStart, data.tokenEnd);
+
+    // 13 - 16        Atom            Atom name.
+    TokenBuilder.addToken(sites.auth_atom_id, Tokenizer.trim(data, s + 12, s + 16));
+
+    // 17             Character       Alternate location indicator.
+    if (str.charCodeAt(s + 16) === 32) { // ' '
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    } else {
+        TokenBuilder.add(sites.label_alt_id, s + 16, s + 17);
+    }
+
+    // 18 - 20        Residue name    Residue name.
+    TokenBuilder.addToken(sites.auth_comp_id, Tokenizer.trim(data, s + 17, s + 20));
+    const residueName = str.substring(data.tokenStart, data.tokenEnd);
+
+    // 22             Character       Chain identifier.
+    TokenBuilder.add(sites.auth_asym_id, s + 21, s + 22);
+
+    // 23 - 26        Integer         Residue sequence number.
+    // TODO: support HEX
+    TokenBuilder.addToken(sites.auth_seq_id, Tokenizer.trim(data, s + 22, s + 26));
+
+    // 27             AChar           Code for insertion of residues.
+    if (str.charCodeAt(s + 26) === 32) { // ' '
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    } else {
+        TokenBuilder.add(sites.label_alt_id, s + 26, s + 27);
+    }
+
+    // 31 - 38        Real(8.3)       Orthogonal coordinates for X in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_x, Tokenizer.trim(data, s + 30, s + 38));
+
+    // 39 - 46        Real(8.3)       Orthogonal coordinates for Y in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_y, Tokenizer.trim(data, s + 38, s + 46));
+
+    // 47 - 54        Real(8.3)       Orthogonal coordinates for Z in Angstroms.
+    TokenBuilder.addToken(sites.Cartn_z, Tokenizer.trim(data, s + 46, s + 54));
+
+    // 55 - 60        Real(6.2)       Occupancy.
+    TokenBuilder.addToken(sites.occupancy, Tokenizer.trim(data, s + 54, s + 60));
+
+    // 61 - 66        Real(6.2)       Temperature factor (Default = 0.0).
+    if (length >= 66) {
+        TokenBuilder.addToken(sites.B_iso_or_equiv, Tokenizer.trim(data, s + 60, s + 66));
+    } else {
+        TokenBuilder.add(sites.label_alt_id, 0, 0);
+    }
+
+    // 73 - 76        LString(4)      Segment identifier, left-justified.
+    // ignored
+
+    // 77 - 78        LString(2)      Element symbol, right-justified.
+    if (length >= 78) {
+        Tokenizer.trim(data, s + 76, s + 78);
+
+        if (data.tokenStart < data.tokenEnd) {
+            TokenBuilder.addToken(sites.type_symbol, data);
+        } else {
+            // "guess" the symbol
+            TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+        }
+    } else {
+        TokenBuilder.add(sites.type_symbol, s + 12, s + 13);
+    }
+
+    sites.label_entity_id[sites.index] = getEntityId(residueName, isHet);
+    sites.pdbx_PDB_model_num[sites.index] = model;
+
+    sites.index++;
+}
+
+export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
+    const { lines } = pdb;
+    const { data, indices } = lines;
+    const tokenizer = Tokenizer(data);
+
+    // Count the atoms
+    let atomCount = 0;
+    for (let i = 0, _i = lines.count; i < _i; i++) {
+        const s = indices[2 * i], e = indices[2 * i + 1];
+        switch (data[s]) {
+            case 'A':
+                if (substringStartsWith(data, s, e, 'ATOM  ')) atomCount++;
+                break;
+            case 'H':
+                if (substringStartsWith(data, s, e, 'HETATM')) atomCount++;
+                break;
+        }
+    }
+
+    const atom_site = atom_site_template(data, atomCount);
+
+    const helperCategories: CifCategory[] = [];
+
+    let modelNum = 0, modelStr = '';
+
+    for (let i = 0, _i = lines.count; i < _i; i++) {
+        let s = indices[2 * i], e = indices[2 * i + 1];
+        switch (data[s]) {
+            case 'A':
+                if (!substringStartsWith(data, s, e, 'ATOM  ')) continue;
+                if (!modelNum) { modelNum++; modelStr = '' + modelNum; }
+                addAtom(atom_site, modelStr, tokenizer, s, e, false);
+                break;
+            case 'C':
+                if (substringStartsWith(data, s, e, 'CRYST1')) {
+                    helperCategories.push(...parseCryst1(pdb.id || '?', data.substring(s, e)));
+                }
+                // TODO CONNECT records => struct_conn
+                // TODO COMPND records => entity
+                break;
+            case 'H':
+                if (substringStartsWith(data, s, e, 'HETATM')) {
+                    if (!modelNum) { modelNum++; modelStr = '' + modelNum; }
+                    addAtom(atom_site, modelStr, tokenizer, s, e, true);
+                } else if (substringStartsWith(data, s, e, 'HELIX')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'HELIX')) break;
+                        j++;
+                    }
+                    helperCategories.push(parseHelix(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO HETNAM records => chem_comp (at least partially, needs to be completed with common bases and amino acids)
+                break;
+            case 'M':
+                if (substringStartsWith(data, s, e, 'MODEL ')) {
+                    modelNum++;
+                    modelStr = '' + modelNum;
+                }
+                if (substringStartsWith(data, s, e, 'MTRIX')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'MTRIX')) break;
+                        j++;
+                    }
+                    helperCategories.push(...parseMtrix(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO MODRES records => pdbx_struct_mod_residue
+                break;
+            case 'O':
+                // TODO ORIGX record => cif.database_PDB_matrix.origx, cif.database_PDB_matrix.origx_vector
+                break;
+            case 'R':
+                if (substringStartsWith(data, s, e, 'REMARK 350')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'REMARK 350')) break;
+                        j++;
+                    }
+                    helperCategories.push(...parseRemark350(lines, i, j));
+                    i = j - 1;
+                }
+                break;
+            case 'S':
+                if (substringStartsWith(data, s, e, 'SHEET')) {
+                    let j = i + 1;
+                    while (true) {
+                        s = indices[2 * j]; e = indices[2 * j + 1];
+                        if (!substringStartsWith(data, s, e, 'SHEET')) break;
+                        j++;
+                    }
+                    helperCategories.push(parseSheet(lines, i, j));
+                    i = j - 1;
+                }
+                // TODO SCALE record => cif.atom_sites.fract_transf_matrix, cif.atom_sites.fract_transf_vector
+                break;
+        }
+    }
+
+    const categories = {
+        entity: CifCategory.ofFields('entity', _entity()),
+        atom_site: CifCategory.ofFields('atom_site', _atom_site(atom_site))
+    } as any;
+
+    for (const c of helperCategories) {
+        categories[c.name] = c;
+    }
+
+    return {
+        header: pdb.id || 'PDB',
+        categoryNames: Object.keys(categories),
+        categories
+    };
+}
\ No newline at end of file
diff --git a/src/mol-model/volume/formats/ccp4.ts b/src/mol-model-formats/volume/ccp4.ts
similarity index 59%
rename from src/mol-model/volume/formats/ccp4.ts
rename to src/mol-model-formats/volume/ccp4.ts
index febb0fc604f172b4a7195b0b0bc06e5492657403..118205173abad9610ea2f2784990a23e29de2728 100644
--- a/src/mol-model/volume/formats/ccp4.ts
+++ b/src/mol-model-formats/volume/ccp4.ts
@@ -4,16 +4,40 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { VolumeData } from '../data'
+import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
-import { Ccp4File } from 'mol-io/reader/ccp4/schema';
+import { Ccp4File, Ccp4Header } from 'mol-io/reader/ccp4/schema';
 import { degToRad } from 'mol-math/misc';
+import { getCcp4ValueType } from 'mol-io/reader/ccp4/parser';
+import { TypedArrayValueType } from 'mol-io/common/typed-array';
 
-function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
-    return Task.create<VolumeData>('Parse Volume Data', async ctx => {
+/** When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START */
+export function getCcp4Origin(header: Ccp4Header) {
+    let gridOrigin: number[]
+    if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) {
+        gridOrigin = [header.NCSTART, header.NRSTART, header.NSSTART];
+    } else {
+        gridOrigin = [header.originX, header.originY, header.originZ];
+    }
+    return gridOrigin
+}
+
+function getTypedArrayCtor(header: Ccp4Header) {
+    const valueType = getCcp4ValueType(header)
+    switch (valueType) {
+        case TypedArrayValueType.Float32: return Float32Array;
+        case TypedArrayValueType.Int8: return Int8Array;
+        case TypedArrayValueType.Int16: return Int16Array;
+    }
+    throw Error(`${valueType} is not a supported value format.`);
+}
+
+export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
+    return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { header, values } = source;
+        console.log({ header, values })
         const size = Vec3.create(header.xLength, header.yLength, header.zLength)
         if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize)
         const angles = Vec3.create(degToRad(header.alpha), degToRad(header.beta), degToRad(header.gamma))
@@ -24,19 +48,12 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V
 
         const grid = [header.NX, header.NY, header.NZ];
         const extent = normalizeOrder([header.NC, header.NR, header.NS]);
-
-        let gridOrigin: number[]
-        if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) {
-            gridOrigin = normalizeOrder([header.NCSTART, header.NRSTART, header.NSSTART]);
-        } else {
-            // When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START
-            gridOrigin = [header.originX, header.originY, header.originZ];
-        }
+        const gridOrigin = normalizeOrder(getCcp4Origin(header));
 
         const origin_frac = Vec3.create(gridOrigin[0] / grid[0], gridOrigin[1] / grid[1], gridOrigin[2] / grid[2]);
         const dimensions_frac = Vec3.create(extent[0] / grid[0], extent[1] / grid[1], extent[2] / grid[2]);
 
-        const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), header.MODE === 0 ? Int8Array : Float32Array);
+        const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), getTypedArrayCtor(header));
         const data = Tensor.create(space, Tensor.Data1(values));
 
         // TODO Calculate stats? When to trust header data?
@@ -55,6 +72,4 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V
             }
         };
     });
-}
-
-export { volumeFromCcp4 }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/mol-model/volume/formats/density-server.ts b/src/mol-model-formats/volume/density-server.ts
similarity index 88%
rename from src/mol-model/volume/formats/density-server.ts
rename to src/mol-model-formats/volume/density-server.ts
index c01282e8e9d69ebd0cb24e35fef3730da9b0f337..315bc33a6edbed6f54057da8595ac31e45cd7b54 100644
--- a/src/mol-model/volume/formats/density-server.ts
+++ b/src/mol-model-formats/volume/density-server.ts
@@ -5,13 +5,13 @@
  */
 
 import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server'
-import { VolumeData } from '../data'
+import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
 
-function parseDensityServerData(source: DensityServer_Data_Database): Task<VolumeData> {
-    return Task.create<VolumeData>('Parse Volume Data', async ctx => {
+function volumeFromDensityServerData(source: DensityServer_Data_Database): Task<VolumeData> {
+    return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { volume_data_3d_info: info, volume_data_3d: values } = source;
         const cell = SpacegroupCell.create(
             info.spacegroup_number.value(0),
@@ -47,4 +47,4 @@ function parseDensityServerData(source: DensityServer_Data_Database): Task<Volum
     });
 }
 
-export { parseDensityServerData }
\ No newline at end of file
+export { volumeFromDensityServerData }
\ No newline at end of file
diff --git a/src/mol-model-formats/volume/dsn6.ts b/src/mol-model-formats/volume/dsn6.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b256fe599afc421722d8943defc96fccc4fe4f4f
--- /dev/null
+++ b/src/mol-model-formats/volume/dsn6.ts
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { VolumeData } from 'mol-model/volume/data'
+import { Task } from 'mol-task';
+import { SpacegroupCell, Box3D } from 'mol-math/geometry';
+import { Tensor, Vec3 } from 'mol-math/linear-algebra';
+import { degToRad } from 'mol-math/misc';
+import { Dsn6File } from 'mol-io/reader/dsn6/schema';
+import { arrayMin, arrayMax, arrayMean, arrayRms } from 'mol-util/array';
+
+function volumeFromDsn6(source: Dsn6File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
+    return Task.create<VolumeData>('Create Volume Data', async ctx => {
+        const { header, values } = source;
+        const size = Vec3.create(header.xlen, header.ylen, header.zlen)
+        if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize)
+        const angles = Vec3.create(degToRad(header.alpha), degToRad(header.beta), degToRad(header.gamma))
+        const cell = SpacegroupCell.create('P 1', size, angles)
+
+        const grid = [header.xRate, header.yRate, header.zRate];
+        const extent = [header.xExtent, header.yExtent, header.zExtent];
+
+        const gridOrigin = [header.xStart, header.yStart, header.zStart];
+
+        const origin_frac = Vec3.create(gridOrigin[0] / grid[0], gridOrigin[1] / grid[1], gridOrigin[2] / grid[2]);
+        const dimensions_frac = Vec3.create(extent[0] / grid[0], extent[1] / grid[1], extent[2] / grid[2]);
+
+        const space = Tensor.Space(extent, [0, 1, 2], Float32Array);
+        const data = Tensor.create(space, Tensor.Data1(values));
+
+        return {
+            cell,
+            fractionalBox: Box3D.create(origin_frac, Vec3.add(Vec3.zero(), origin_frac, dimensions_frac)),
+            data,
+            dataStats: {
+                min: arrayMin(values),
+                max: arrayMax(values),
+                mean: arrayMean(values),
+                sigma: header.sigma !== undefined ? header.sigma : arrayRms(values)
+            }
+        };
+    });
+}
+
+export { volumeFromDsn6 }
\ No newline at end of file
diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts
new file mode 100644
index 0000000000000000000000000000000000000000..85f9306753784f838dbc00dce60b168394c5b6be
--- /dev/null
+++ b/src/mol-model-props/common/custom-element-property.ts
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ElementIndex, Model, ModelPropertyDescriptor } from 'mol-model/structure';
+import { StructureElement } from 'mol-model/structure/structure';
+import { Location } from 'mol-model/location';
+import { CustomPropertyRegistry } from './custom-property-registry';
+import { Task } from 'mol-task';
+import { ThemeDataContext, ThemeProvider } from 'mol-theme/theme';
+import { ColorTheme, LocationColor } from 'mol-theme/color';
+import { Color } from 'mol-util/color';
+import { TableLegend } from 'mol-util/color/tables';
+import { Loci } from 'mol-model/loci';
+import { OrderedSet } from 'mol-data/int';
+
+export { CustomElementProperty };
+
+namespace CustomElementProperty {
+    export interface CreateParams<T> {
+        isStatic: boolean,
+        name: string,
+        autoAttach?: boolean,
+        display: string,
+        attachableTo?: (model: Model) => boolean,
+        getData(model: Model): Map<ElementIndex, T> | Promise<Map<ElementIndex, T>>,
+        format?(e: T): string | undefined,
+        coloring?: {
+            getColor: (e: T) => Color,
+            defaultColor: Color
+        }
+    }
+
+    export function create<T>(params: CreateParams<T>) {
+        const name = params.name;
+
+        const Descriptor = ModelPropertyDescriptor({
+            isStatic: params.isStatic,
+            name: params.name,
+        });
+
+        function attach(model: Model) {
+            return Task.create(`Attach ${params.display}`, async () => {
+                try {
+                    if (model.customProperties.has(Descriptor)) return true;
+
+                    const data = await params.getData(model);
+
+                    if (params.isStatic) {
+                        model._staticPropertyData[name] = data;
+                    } else {
+                        model._dynamicPropertyData[name] = data;
+                    }
+
+                    model.customProperties.add(Descriptor);
+
+                    return true;
+                } catch (e) {
+                    console.warn('Attach Property', e);
+                    return false;
+                }
+            })
+        }
+
+        function getStatic(e: StructureElement) { return e.unit.model._staticPropertyData[name].get(e.element); }
+        function getDynamic(e: StructureElement) { return e.unit.model._staticPropertyData[name].get(e.element); }
+
+        const propertyProvider: CustomPropertyRegistry.Provider = {
+            option: [name, params.display],
+            descriptor: Descriptor,
+            defaultSelected: !!params.autoAttach,
+            attachableTo: params.attachableTo || (() => true),
+            attach
+        };
+
+        const get = params.isStatic ? getStatic : getDynamic;
+
+        function has(model: Model) { return model.customProperties.has(Descriptor); }
+
+        function Coloring(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
+            let color: LocationColor;
+            const getColor = params.coloring!.getColor;
+            const defaultColor = params.coloring!.defaultColor;
+
+            if (ctx.structure && !ctx.structure.isEmpty && has(ctx.structure.models[0])) {
+                color = (location: Location) => {
+                    if (StructureElement.isLocation(location)) {
+                        const e = get(location);
+                        if (typeof e !== 'undefined') return getColor(e);
+                    }
+                    return defaultColor;
+                }
+            } else {
+                color = () => defaultColor;
+            }
+
+            return {
+                factory: Coloring,
+                granularity: 'group',
+                color: color,
+                props: props,
+                description: 'Assign element colors based on the provided data.',
+                legend: TableLegend([])
+            };
+        }
+
+        const colorTheme: ThemeProvider<ColorTheme<{}>, {}> = {
+            label: params.display,
+            factory: Coloring,
+            getParams: () => ({}),
+            defaultValues: {},
+            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && has(ctx.structure.models[0])
+        }
+
+        function LabelProvider(loci: Loci): string | undefined {
+            if (loci.kind === 'element-loci') {
+                const e = loci.elements[0];
+                if (!has(e.unit.model)) return void 0;
+                return params.format!(get(StructureElement.create(e.unit, e.unit.elements[OrderedSet.getAt(e.indices, 0)])));
+            }
+            return void 0;
+        }
+
+        return {
+            Descriptor,
+            attach,
+            get,
+            propertyProvider,
+            colorTheme: params.coloring ? colorTheme : void 0,
+            labelProvider: params.format ? LabelProvider : ((loci: Loci) => void 0)
+        };
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/util/custom-prop-registry.ts b/src/mol-model-props/common/custom-property-registry.ts
similarity index 100%
rename from src/mol-plugin/util/custom-prop-registry.ts
rename to src/mol-model-props/common/custom-property-registry.ts
diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts
index 12cb6d018b773121657e539bc0b86f02dcf47635..b534c465ff624e5793f4de87e7bc437c099073cd 100644
--- a/src/mol-model-props/pdbe/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/structure-quality-report.ts
@@ -83,7 +83,7 @@ export namespace StructureQualityReport {
         }
     }
 
-    export function createAttachTask(mapUrl: (model: Model) => string, fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
+    export function createAttachTask(mapUrl: (model: Model) => string, fetch: import('mol-util/data-source').AjaxTask) {
         return (model: Model) => Task.create('PDBe Structure Quality Report', async ctx => {
             if (get(model)) return true;
 
@@ -97,7 +97,7 @@ export namespace StructureQualityReport {
             // } else
             {
                 const url = mapUrl(model);
-                const dataStr = await fetch(url, 'string').runInContext(ctx) as string;
+                const dataStr = await fetch({ url }).runInContext(ctx) as string;
                 const data = JSON.parse(dataStr)[model.label.toLowerCase()];
                 if (!data) return false;
                 info = PropertyWrapper.createInfo();
diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
index a527e58f7797addf3ad4b2a3de9109c5179f330b..928298bd293b60de2d5673ae174134a9dca1d75a 100644
--- a/src/mol-model-props/pdbe/themes/structure-quality-report.ts
+++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts
@@ -31,7 +31,7 @@ const ValidationColorTable: [string, Color][] = [
 export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
     let color: LocationColor
 
-    if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
+    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
         const getIssues = StructureQualityReport.getIssues;
         color = (location: Location) => {
             if (StructureElement.isLocation(location)) {
diff --git a/src/mol-model-props/rcsb/assembly-symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts
index 5d5841bfc59b4077c52eb85041ba0bb6c063dc25..c67ed0aca0b78727b2d073b60563f7b1f128d76b 100644
--- a/src/mol-model-props/rcsb/assembly-symmetry.ts
+++ b/src/mol-model-props/rcsb/assembly-symmetry.ts
@@ -44,18 +44,17 @@ function createDatabaseFromJson(assemblies: ReadonlyArray<AssemblySymmetryGraphQ
     let id = 1 // start feature ids at 1
     let clusterCount = 0
     for (let i = 0, il = assemblies.length; i < il; ++i) {
-        const { pdbx_struct_assembly, rcsb_struct_symmetry, rcsb_struct_symmetry_provenance } = assemblies[i]
-        if (!pdbx_struct_assembly || !rcsb_struct_symmetry ||!rcsb_struct_symmetry_provenance) continue
+        const { pdbx_struct_assembly, rcsb_struct_symmetry } = assemblies[i]
+        if (!pdbx_struct_assembly || !rcsb_struct_symmetry) continue
         const assembly_id = pdbx_struct_assembly.id
         for (let j = 0, jl = rcsb_struct_symmetry.length; j < jl; ++j) {
             const rss = rcsb_struct_symmetry[j]! // TODO upstream, array members should not be nullable
             featureRows.push({
                 id,
                 assembly_id,
-                provenance: rcsb_struct_symmetry_provenance,
-                type: rss.type,
+                type: rss.type as SymmetryType,
                 stoichiometry: rss.stoichiometry as string[],  // TODO upstream, array members should not be nullable
-                kind: rss.kind,
+                kind: rss.kind as SymmetryKind,
                 symbol: rss.symbol,
                 oligomeric_state: rss.oligomeric_state
             })
@@ -181,7 +180,10 @@ export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetr
     }
 }
 
-const Client = new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, (url: string, type: 'string' | 'binary', body?: string) => ajaxGet({ url, type, body }) )
+type SymmetryKind = 'GLOBAL' | 'LOCAL' | 'PSEUDO'
+type SymmetryType = 'ASYMMETRIC' | 'CYCLIC' | 'DIHEDRAL' | 'HELICAL' | 'ICOSAHEDRAL' | 'OCTAHEDRAL' | 'TETRAHEDRAL'
+
+const Client = new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, ajaxGet)
 
 export namespace AssemblySymmetry {
     export function is(x: any): x is AssemblySymmetry {
@@ -200,10 +202,8 @@ export namespace AssemblySymmetry {
              * The value 'deposited' refers to the coordinates as given in the file.
              * */
             assembly_id: str,
-            /** Name and version of software used to calculate assembly symmetry */
-            provenance: str,
             /** Type of protein symmetry */
-            kind: Aliased<'GLOBAL' | 'LOCAL' | 'PSEUDO'>(str),
+            kind: Aliased<SymmetryKind>(str),
             /** Quantitative description of every individual subunit in a given protein */
             stoichiometry: List(',', x => x),
             /**
@@ -212,7 +212,7 @@ export namespace AssemblySymmetry {
              */
             symbol: str,
             /** Point group or helical symmetry */
-            type: Aliased<'ASYMMETRIC' | 'CYCLIC' | 'DIHEDRAL' | 'HELICAL' | 'ICOSAHEDRAL' | 'OCTAHEDRAL' | 'TETRAHEDRAL'>(str),
+            type: Aliased<SymmetryType>(str),
             /**
              * Oligomeric state refers to a composition of subunits in quaternary structure.
              * Quaternary structure may be composed either exclusively of several copies of identical
@@ -283,7 +283,7 @@ export namespace AssemblySymmetry {
         return true;
     }
 
-    export function createAttachTask(fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
+    export function createAttachTask(fetch: import('mol-util/data-source').AjaxTask) {
         return (model: Model) => Task.create('RCSB Assembly Symmetry', async ctx => {
             if (get(model)) return true;
 
diff --git a/src/mol-model-props/rcsb/graphql/symmetry.gql.ts b/src/mol-model-props/rcsb/graphql/symmetry.gql.ts
index 7c9fe867fba16519314ab819357d46675e29225a..87aec81b1deb5af462a61f845a57b474342a50f8 100644
--- a/src/mol-model-props/rcsb/graphql/symmetry.gql.ts
+++ b/src/mol-model-props/rcsb/graphql/symmetry.gql.ts
@@ -1,8 +1,4 @@
- // workaround so the query gets found by the codegen
-function gql (strs: TemplateStringsArray) { return strs.raw.join('') }
-
-export default
-gql`query AssemblySymmetry($pdbId: String!) {
+export default `query AssemblySymmetry($pdbId: String!) {
     assemblies(pdbId: $pdbId) {
         pdbx_struct_assembly {
             id
@@ -26,6 +22,5 @@ gql`query AssemblySymmetry($pdbId: String!) {
             symbol
             type
         }
-        rcsb_struct_symmetry_provenance
     }
 }`
\ No newline at end of file
diff --git a/src/mol-model-props/rcsb/graphql/types.ts b/src/mol-model-props/rcsb/graphql/types.ts
index 1c89e3aae72dd79fc4dcabb22fd7c9b23ca8a1f9..fc3e89fbf2463ac3c5746528728fa3b67614c76a 100644
--- a/src/mol-model-props/rcsb/graphql/types.ts
+++ b/src/mol-model-props/rcsb/graphql/types.ts
@@ -1,1197 +1,11 @@
-/* tslint:disable */
-/** Generated in 2018-10-31T12:19:29-07:00 */
-
-// ====================================================
-// START: Typescript template
-// ====================================================
-
-// ====================================================
-// Scalars
-// ====================================================
+// Generated in 2019-03-01T14:48:33-08:00
+export type Maybe<T> = T | null;
 
 /** Built-in scalar representing an instant in time */
 export type Date = any;
 
-// ====================================================
-// Enums
-// ====================================================
-
-export enum ChemCompPdbxIdealCoordinatesMissingFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum ChemCompPdbxModelCoordinatesMissingFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum ChemCompPdbxProcessingSite {
-  EBI = "EBI",
-  PDBE = "PDBE",
-  PDBJ = "PDBJ",
-  RCSB = "RCSB"
-}
-
-export enum ChemCompPdbxReleaseStatus {
-  DEL = "DEL",
-  HOLD = "HOLD",
-  HPUB = "HPUB",
-  OBS = "OBS",
-  REF_ONLY = "REF_ONLY",
-  REL = "REL"
-}
-
-export enum ChemCompType {
-  DNA_LINKING = "DNA_LINKING",
-  DNA_OH_3_PRIME_TERMINUS = "DNA_OH_3_PRIME_TERMINUS",
-  DNA_OH_5_PRIME_TERMINUS = "DNA_OH_5_PRIME_TERMINUS",
-  D_BETA_PEPTIDE_C_GAMMA_LINKING = "D_BETA_PEPTIDE_C_GAMMA_LINKING",
-  D_GAMMA_PEPTIDE_C_DELTA_LINKING = "D_GAMMA_PEPTIDE_C_DELTA_LINKING",
-  D_PEPTIDE_COOH_CARBOXY_TERMINUS = "D_PEPTIDE_COOH_CARBOXY_TERMINUS",
-  D_PEPTIDE_LINKING = "D_PEPTIDE_LINKING",
-  D_PEPTIDE_NH_3_AMINO_TERMINUS = "D_PEPTIDE_NH_3_AMINO_TERMINUS",
-  D_SACCHARIDE = "D_SACCHARIDE",
-  D_SACCHARIDE_1_4_AND_1_4_LINKING = "D_SACCHARIDE_1_4_AND_1_4_LINKING",
-  D_SACCHARIDE_1_4_AND_1_6_LINKING = "D_SACCHARIDE_1_4_AND_1_6_LINKING",
-  L_BETA_PEPTIDE_C_GAMMA_LINKING = "L_BETA_PEPTIDE_C_GAMMA_LINKING",
-  L_DNA_LINKING = "L_DNA_LINKING",
-  L_GAMMA_PEPTIDE_C_DELTA_LINKING = "L_GAMMA_PEPTIDE_C_DELTA_LINKING",
-  L_PEPTIDE_COOH_CARBOXY_TERMINUS = "L_PEPTIDE_COOH_CARBOXY_TERMINUS",
-  L_PEPTIDE_LINKING = "L_PEPTIDE_LINKING",
-  L_PEPTIDE_NH_3_AMINO_TERMINUS = "L_PEPTIDE_NH_3_AMINO_TERMINUS",
-  L_RNA_LINKING = "L_RNA_LINKING",
-  L_SACCHARIDE = "L_SACCHARIDE",
-  L_SACCHARIDE_1_4_AND_1_4_LINKING = "L_SACCHARIDE_1_4_AND_1_4_LINKING",
-  L_SACCHARIDE_1_4_AND_1_6_LINKING = "L_SACCHARIDE_1_4_AND_1_6_LINKING",
-  NON_POLYMER = "NON_POLYMER",
-  OTHER = "OTHER",
-  PEPTIDE_LIKE = "PEPTIDE_LIKE",
-  PEPTIDE_LINKING = "PEPTIDE_LINKING",
-  RNA_LINKING = "RNA_LINKING",
-  RNA_OH_3_PRIME_TERMINUS = "RNA_OH_3_PRIME_TERMINUS",
-  RNA_OH_5_PRIME_TERMINUS = "RNA_OH_5_PRIME_TERMINUS",
-  SACCHARIDE = "SACCHARIDE"
-}
-
-export enum ChemCompAtomPdbxAromaticFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum ChemCompAtomPdbxLeavingAtomFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum ChemCompAtomPdbxPolymerType {
-  NON_POLYMER = "NON_POLYMER",
-  POLYMER = "POLYMER"
-}
-
-export enum ChemCompAtomPdbxStereoConfig {
-  N = "N",
-  R = "R",
-  S = "S"
-}
-
-export enum ChemCompBondPdbxAromaticFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum ChemCompBondPdbxStereoConfig {
-  E = "E",
-  N = "N",
-  Z = "Z"
-}
-
-export enum ChemCompBondValueOrder {
-  AROM = "AROM",
-  DELO = "DELO",
-  DOUB = "DOUB",
-  PI = "PI",
-  POLY = "POLY",
-  QUAD = "QUAD",
-  SING = "SING",
-  TRIP = "TRIP"
-}
-
-export enum PdbxChemCompAuditActionType {
-  CREATE_COMPONENT = "CREATE_COMPONENT",
-  INITIAL_RELEASE = "INITIAL_RELEASE",
-  MODIFY_AROMATIC_FLAG = "MODIFY_AROMATIC_FLAG",
-  MODIFY_ATOM_ID = "MODIFY_ATOM_ID",
-  MODIFY_CHARGE = "MODIFY_CHARGE",
-  MODIFY_COMPONENT_ATOM_ID = "MODIFY_COMPONENT_ATOM_ID",
-  MODIFY_COMPONENT_COMP_ID = "MODIFY_COMPONENT_COMP_ID",
-  MODIFY_COORDINATES = "MODIFY_COORDINATES",
-  MODIFY_DESCRIPTOR = "MODIFY_DESCRIPTOR",
-  MODIFY_FORMAL_CHARGE = "MODIFY_FORMAL_CHARGE",
-  MODIFY_FORMULA = "MODIFY_FORMULA",
-  MODIFY_IDENTIFIER = "MODIFY_IDENTIFIER",
-  MODIFY_INTERNAL_TYPE = "MODIFY_INTERNAL_TYPE",
-  MODIFY_LEAVING_ATOM_FLAG = "MODIFY_LEAVING_ATOM_FLAG",
-  MODIFY_LINKING_TYPE = "MODIFY_LINKING_TYPE",
-  MODIFY_MODEL_COORDINATES_CODE = "MODIFY_MODEL_COORDINATES_CODE",
-  MODIFY_NAME = "MODIFY_NAME",
-  MODIFY_ONE_LETTER_CODE = "MODIFY_ONE_LETTER_CODE",
-  MODIFY_PARENT_RESIDUE = "MODIFY_PARENT_RESIDUE",
-  MODIFY_PROCESSING_SITE = "MODIFY_PROCESSING_SITE",
-  MODIFY_SUBCOMPONENT_LIST = "MODIFY_SUBCOMPONENT_LIST",
-  MODIFY_SYNONYMS = "MODIFY_SYNONYMS",
-  MODIFY_VALUE_ORDER = "MODIFY_VALUE_ORDER",
-  OBSOLETE_COMPONENT = "OBSOLETE_COMPONENT",
-  OTHER_MODIFICATION = "OTHER_MODIFICATION"
-}
-
-export enum PdbxChemCompDescriptorType {
-  IN_CH_I = "IN_CH_I",
-  IN_CH_I_CHARGE = "IN_CH_I_CHARGE",
-  IN_CH_I_FIXEDH = "IN_CH_I_FIXEDH",
-  IN_CH_I_ISOTOPE = "IN_CH_I_ISOTOPE",
-  IN_CH_I_KEY = "IN_CH_I_KEY",
-  IN_CH_I_MAIN = "IN_CH_I_MAIN",
-  IN_CH_I_MAIN_CONNECT = "IN_CH_I_MAIN_CONNECT",
-  IN_CH_I_MAIN_FORMULA = "IN_CH_I_MAIN_FORMULA",
-  IN_CH_I_MAIN_HATOM = "IN_CH_I_MAIN_HATOM",
-  IN_CH_I_RECONNECT = "IN_CH_I_RECONNECT",
-  IN_CH_I_STEREO = "IN_CH_I_STEREO",
-  SMILES = "SMILES",
-  SMILES_CANNONICAL = "SMILES_CANNONICAL",
-  SMILES_CANONICAL = "SMILES_CANONICAL"
-}
-
-export enum PdbxChemCompIdentifierType {
-  CAS_REGISTRY_NUMBER = "CAS_REGISTRY_NUMBER",
-  COMMON_NAME = "COMMON_NAME",
-  CONDENSED_IUPAC_CARB_NAME = "CONDENSED_IUPAC_CARB_NAME",
-  IUPAC_CARB_NAME = "IUPAC_CARB_NAME",
-  MDL_IDENTIFIER = "MDL_IDENTIFIER",
-  PUBCHEM_IDENTIFIER = "PUBCHEM_IDENTIFIER",
-  SYNONYM = "SYNONYM",
-  SYSTEMATIC_NAME = "SYSTEMATIC_NAME"
-}
-
-export enum PdbxStructAssemblyRcsbCandidateAssembly {
-  N = "N",
-  Y = "Y"
-}
-
-export enum PdbxStructAssemblyRcsbDetails {
-  AUTHOR_AND_SOFTWARE_DEFINED_ASSEMBLY = "AUTHOR_AND_SOFTWARE_DEFINED_ASSEMBLY",
-  AUTHOR_DEFINED_ASSEMBLY = "AUTHOR_DEFINED_ASSEMBLY",
-  SOFTWARE_DEFINED_ASSEMBLY = "SOFTWARE_DEFINED_ASSEMBLY"
-}
-
-export enum PdbxStructAssemblyAuthEvidenceExperimentalSupport {
-  ASSAY_FOR_OLIGOMERIZATION = "ASSAY_FOR_OLIGOMERIZATION",
-  CROSS_LINKING = "CROSS_LINKING",
-  EQUILIBRIUM_CENTRIFUGATION = "EQUILIBRIUM_CENTRIFUGATION",
-  FLUORESCENCE_RESONANCE_ENERGY_TRANSFER = "FLUORESCENCE_RESONANCE_ENERGY_TRANSFER",
-  GEL_FILTRATION = "GEL_FILTRATION",
-  HOMOLOGY = "HOMOLOGY",
-  IMMUNOPRECIPITATION = "IMMUNOPRECIPITATION",
-  ISOTHERMAL_TITRATION_CALORIMETRY = "ISOTHERMAL_TITRATION_CALORIMETRY",
-  LIGHT_SCATTERING = "LIGHT_SCATTERING",
-  MASS_SPECTROMETRY = "MASS_SPECTROMETRY",
-  MICROSCOPY = "MICROSCOPY",
-  NATIVE_GEL_ELECTROPHORESIS = "NATIVE_GEL_ELECTROPHORESIS",
-  NONE = "NONE",
-  SAXS = "SAXS",
-  SCANNING_TRANSMISSION_ELECTRON_MICROSCOPY = "SCANNING_TRANSMISSION_ELECTRON_MICROSCOPY",
-  SURFACE_PLASMON_RESONANCE = "SURFACE_PLASMON_RESONANCE"
-}
-
-export enum PdbxStructAssemblyPropType {
-  ABSA_A_2 = "ABSA_A_2",
-  MORE = "MORE",
-  SSA_A_2 = "SSA_A_2"
-}
-
-export enum PdbxStructOperListType {
-  BUILD_2_D_CRYSTAL_ASYMMETRIC_UNIT = "BUILD_2_D_CRYSTAL_ASYMMETRIC_UNIT",
-  BUILD_3_D_CRYSTAL_ASYMMETRIC_UNIT = "BUILD_3_D_CRYSTAL_ASYMMETRIC_UNIT",
-  BUILD_HELICAL_ASYMMETRIC_UNIT = "BUILD_HELICAL_ASYMMETRIC_UNIT",
-  BUILD_POINT_ASYMMETRIC_UNIT = "BUILD_POINT_ASYMMETRIC_UNIT",
-  CRYSTAL_SYMMETRY_OPERATION = "CRYSTAL_SYMMETRY_OPERATION",
-  HELICAL_SYMMETRY_OPERATION = "HELICAL_SYMMETRY_OPERATION",
-  IDENTITY_OPERATION = "IDENTITY_OPERATION",
-  POINT_SYMMETRY_OPERATION = "POINT_SYMMETRY_OPERATION",
-  TRANSFORM_TO_2_D_CRYSTAL_FRAME = "TRANSFORM_TO_2_D_CRYSTAL_FRAME",
-  TRANSFORM_TO_3_D_CRYSTAL_FRAME = "TRANSFORM_TO_3_D_CRYSTAL_FRAME",
-  TRANSFORM_TO_CRYSTAL_FRAME = "TRANSFORM_TO_CRYSTAL_FRAME",
-  TRANSFORM_TO_HELICAL_FRAME = "TRANSFORM_TO_HELICAL_FRAME",
-  TRANSFORM_TO_POINT_FRAME = "TRANSFORM_TO_POINT_FRAME",
-  _2_D_CRYSTAL_SYMMETRY_OPERATION = "_2_D_CRYSTAL_SYMMETRY_OPERATION",
-  _3_D_CRYSTAL_SYMMETRY_OPERATION = "_3_D_CRYSTAL_SYMMETRY_OPERATION"
-}
-
-export enum RcsbStructSymmetryKind {
-  GLOBAL = "GLOBAL",
-  LOCAL = "LOCAL",
-  PSEUDO = "PSEUDO"
-}
-
-export enum RcsbStructSymmetryType {
-  ASYMMETRIC = "ASYMMETRIC",
-  CYCLIC = "CYCLIC",
-  DIHEDRAL = "DIHEDRAL",
-  HELICAL = "HELICAL",
-  ICOSAHEDRAL = "ICOSAHEDRAL",
-  OCTAHEDRAL = "OCTAHEDRAL",
-  TETRAHEDRAL = "TETRAHEDRAL"
-}
-
-export enum CitationCoordinateLinkage {
-  N = "N",
-  NO = "NO",
-  Y = "Y",
-  YES = "YES"
-}
-
-export enum CitationUnpublishedFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum DiffrnRadiationPdbxMonochromaticOrLaueMl {
-  L = "L",
-  M = "M"
-}
-
-export enum DiffrnRadiationPdbxScatteringType {
-  ELECTRON = "ELECTRON",
-  NEUTRON = "NEUTRON",
-  X_RAY = "X_RAY"
-}
-
-export enum Em2dCrystalEntitySpaceGroupNameHm {
-  C_1_2 = "C_1_2",
-  C_2_2_2 = "C_2_2_2",
-  P_1 = "P_1",
-  P_1_2 = "P_1_2",
-  P_1_21 = "P_1_21",
-  P_2 = "P_2",
-  P_2_21_21 = "P_2_21_21",
-  P_2_2_2 = "P_2_2_2",
-  P_2_2_21 = "P_2_2_21",
-  P_3 = "P_3",
-  P_3_1_2 = "P_3_1_2",
-  P_3_2_1 = "P_3_2_1",
-  P_4 = "P_4",
-  P_4_21_2 = "P_4_21_2",
-  P_4_2_2 = "P_4_2_2",
-  P_6 = "P_6",
-  P_6_2_2 = "P_6_2_2"
-}
-
-export enum Em3dFittingRefProtocol {
-  AB_INITIO_MODEL = "AB_INITIO_MODEL",
-  BACKBONE_TRACE = "BACKBONE_TRACE",
-  FLEXIBLE_FIT = "FLEXIBLE_FIT",
-  OTHER = "OTHER",
-  RIGID_BODY_FIT = "RIGID_BODY_FIT"
-}
-
-export enum Em3dFittingRefSpace {
-  REAL = "REAL",
-  RECIPROCAL = "RECIPROCAL"
-}
-
-export enum Em3dReconstructionRefinementType {
-  HALF_MAPS_REFINED_AGAINST_SAME_DATA = "HALF_MAPS_REFINED_AGAINST_SAME_DATA",
-  HALF_MAPS_REFINED_INDEPENDENTLY = "HALF_MAPS_REFINED_INDEPENDENTLY",
-  HALF_MAPS_REFINED_INDEPENDENTLY_WITH_FREQUENCY_RANGE_OMITTED = "HALF_MAPS_REFINED_INDEPENDENTLY_WITH_FREQUENCY_RANGE_OMITTED",
-  HALF_MAPS_REFINED_WITH_FREQUENCY_RANGE_OMITTED = "HALF_MAPS_REFINED_WITH_FREQUENCY_RANGE_OMITTED",
-  OTHER = "OTHER"
-}
-
-export enum Em3dReconstructionSymmetryType {
-  HELICAL = "HELICAL",
-  POINT = "POINT",
-  _2_D_CRYSTAL = "_2_D_CRYSTAL",
-  _3_D_CRYSTAL = "_3_D_CRYSTAL"
-}
-
-export enum EmEntityAssemblySource {
-  MULTIPLE_SOURCES = "MULTIPLE_SOURCES",
-  NATURAL = "NATURAL",
-  RECOMBINANT = "RECOMBINANT"
-}
-
-export enum EmExperimentAggregationState {
-  CELL = "CELL",
-  FILAMENT = "FILAMENT",
-  HELICAL_ARRAY = "HELICAL_ARRAY",
-  PARTICLE = "PARTICLE",
-  TISSUE = "TISSUE",
-  _2_D_ARRAY = "_2_D_ARRAY",
-  _3_D_ARRAY = "_3_D_ARRAY"
-}
-
-export enum EmExperimentReconstructionMethod {
-  CRYSTALLOGRAPHY = "CRYSTALLOGRAPHY",
-  HELICAL = "HELICAL",
-  SINGLE_PARTICLE = "SINGLE_PARTICLE",
-  SUBTOMOGRAM_AVERAGING = "SUBTOMOGRAM_AVERAGING",
-  TOMOGRAPHY = "TOMOGRAPHY"
-}
-
-export enum EmImageRecordingDetectorMode {
-  COUNTING = "COUNTING",
-  INTEGRATING = "INTEGRATING",
-  OTHER = "OTHER",
-  SUPER_RESOLUTION = "SUPER_RESOLUTION"
-}
-
-export enum EmImagingAlignmentProcedure {
-  BASIC = "BASIC",
-  COMA_FREE = "COMA_FREE",
-  NONE = "NONE",
-  OTHER = "OTHER",
-  ZEMLIN_TABLEAU = "ZEMLIN_TABLEAU"
-}
-
-export enum EmImagingCryogen {
-  HELIUM = "HELIUM",
-  NITROGEN = "NITROGEN"
-}
-
-export enum EmImagingIlluminationMode {
-  FLOOD_BEAM = "FLOOD_BEAM",
-  OTHER = "OTHER",
-  SPOT_SCAN = "SPOT_SCAN"
-}
-
-export enum EmImagingMicroscopeModel {
-  FEI_MORGAGNI = "FEI_MORGAGNI",
-  FEI_PHILIPS_CM_10 = "FEI_PHILIPS_CM_10",
-  FEI_PHILIPS_CM_12 = "FEI_PHILIPS_CM_12",
-  FEI_PHILIPS_CM_120_T = "FEI_PHILIPS_CM_120_T",
-  FEI_PHILIPS_CM_200_FEG = "FEI_PHILIPS_CM_200_FEG",
-  FEI_PHILIPS_CM_200_FEG_SOPHIE = "FEI_PHILIPS_CM_200_FEG_SOPHIE",
-  FEI_PHILIPS_CM_200_FEG_ST = "FEI_PHILIPS_CM_200_FEG_ST",
-  FEI_PHILIPS_CM_200_FEG_UT = "FEI_PHILIPS_CM_200_FEG_UT",
-  FEI_PHILIPS_CM_200_T = "FEI_PHILIPS_CM_200_T",
-  FEI_PHILIPS_CM_300_FEG_HE = "FEI_PHILIPS_CM_300_FEG_HE",
-  FEI_PHILIPS_CM_300_FEG_ST = "FEI_PHILIPS_CM_300_FEG_ST",
-  FEI_PHILIPS_CM_300_FEG_T = "FEI_PHILIPS_CM_300_FEG_T",
-  FEI_PHILIPS_EM_400 = "FEI_PHILIPS_EM_400",
-  FEI_PHILIPS_EM_420 = "FEI_PHILIPS_EM_420",
-  FEI_POLARA_300 = "FEI_POLARA_300",
-  FEI_TALOS_ARCTICA = "FEI_TALOS_ARCTICA",
-  FEI_TECNAI_10 = "FEI_TECNAI_10",
-  FEI_TECNAI_12 = "FEI_TECNAI_12",
-  FEI_TECNAI_20 = "FEI_TECNAI_20",
-  FEI_TECNAI_ARCTICA = "FEI_TECNAI_ARCTICA",
-  FEI_TECNAI_F_20 = "FEI_TECNAI_F_20",
-  FEI_TECNAI_F_30 = "FEI_TECNAI_F_30",
-  FEI_TECNAI_SPHERA = "FEI_TECNAI_SPHERA",
-  FEI_TECNAI_SPIRIT = "FEI_TECNAI_SPIRIT",
-  FEI_TITAN = "FEI_TITAN",
-  FEI_TITAN_KRIOS = "FEI_TITAN_KRIOS",
-  HITACHI_EF_2000 = "HITACHI_EF_2000",
-  HITACHI_EF_3000 = "HITACHI_EF_3000",
-  HITACHI_HF_2000 = "HITACHI_HF_2000",
-  HITACHI_HF_3000 = "HITACHI_HF_3000",
-  HITACHI_H_7600 = "HITACHI_H_7600",
-  HITACHI_H_9500_SD = "HITACHI_H_9500_SD",
-  JEOL_100_B = "JEOL_100_B",
-  JEOL_100_CX = "JEOL_100_CX",
-  JEOL_1010 = "JEOL_1010",
-  JEOL_1200 = "JEOL_1200",
-  JEOL_1200_EX = "JEOL_1200_EX",
-  JEOL_1200_EXII = "JEOL_1200_EXII",
-  JEOL_1230 = "JEOL_1230",
-  JEOL_1400 = "JEOL_1400",
-  JEOL_2000_EX = "JEOL_2000_EX",
-  JEOL_2000_EXII = "JEOL_2000_EXII",
-  JEOL_2010 = "JEOL_2010",
-  JEOL_2010_F = "JEOL_2010_F",
-  JEOL_2010_HC = "JEOL_2010_HC",
-  JEOL_2010_HT = "JEOL_2010_HT",
-  JEOL_2010_UHR = "JEOL_2010_UHR",
-  JEOL_2011 = "JEOL_2011",
-  JEOL_2100 = "JEOL_2100",
-  JEOL_2100_F = "JEOL_2100_F",
-  JEOL_2200_FS = "JEOL_2200_FS",
-  JEOL_2200_FSC = "JEOL_2200_FSC",
-  JEOL_3000_SFF = "JEOL_3000_SFF",
-  JEOL_3100_FEF = "JEOL_3100_FEF",
-  JEOL_3100_FFC = "JEOL_3100_FFC",
-  JEOL_3200_FS = "JEOL_3200_FS",
-  JEOL_3200_FSC = "JEOL_3200_FSC",
-  JEOL_4000 = "JEOL_4000",
-  JEOL_4000_EX = "JEOL_4000_EX",
-  JEOL_CRYO_ARM_200 = "JEOL_CRYO_ARM_200",
-  JEOL_CRYO_ARM_300 = "JEOL_CRYO_ARM_300",
-  JEOL_KYOTO_3000_SFF = "JEOL_KYOTO_3000_SFF",
-  SIEMENS_SULEIKA = "SIEMENS_SULEIKA",
-  ZEISS_LEO_912 = "ZEISS_LEO_912",
-  ZEISS_LIBRA_120_PLUS = "ZEISS_LIBRA_120_PLUS"
-}
-
-export enum EmImagingMode {
-  BRIGHT_FIELD = "BRIGHT_FIELD",
-  DARK_FIELD = "DARK_FIELD",
-  DIFFRACTION = "DIFFRACTION",
-  OTHER = "OTHER"
-}
-
-export enum EmImagingSpecimenHolderModel {
-  FEI_TITAN_KRIOS_AUTOGRID_HOLDER = "FEI_TITAN_KRIOS_AUTOGRID_HOLDER",
-  FISCHIONE_INSTRUMENTS_DUAL_AXIS_TOMOGRAPHY_HOLDER = "FISCHIONE_INSTRUMENTS_DUAL_AXIS_TOMOGRAPHY_HOLDER",
-  GATAN_626_SINGLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER = "GATAN_626_SINGLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER",
-  GATAN_910_MULTI_SPECIMEN_SINGLE_TILT_CRYO_TRANSFER_HOLDER = "GATAN_910_MULTI_SPECIMEN_SINGLE_TILT_CRYO_TRANSFER_HOLDER",
-  GATAN_914_HIGH_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_TOMOGRAPHY_HOLDER = "GATAN_914_HIGH_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_TOMOGRAPHY_HOLDER",
-  GATAN_915_DOUBLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER = "GATAN_915_DOUBLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER",
-  GATAN_CHDT_3504_DOUBLE_TILT_HIGH_RESOLUTION_NITROGEN_COOLING_HOLDER = "GATAN_CHDT_3504_DOUBLE_TILT_HIGH_RESOLUTION_NITROGEN_COOLING_HOLDER",
-  GATAN_CT_3500_SINGLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER = "GATAN_CT_3500_SINGLE_TILT_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER",
-  GATAN_CT_3500_TR_SINGLE_TILT_ROTATION_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER = "GATAN_CT_3500_TR_SINGLE_TILT_ROTATION_LIQUID_NITROGEN_CRYO_TRANSFER_HOLDER",
-  GATAN_HCHDT_3010_DOUBLE_TILT_HIGH_RESOLUTION_HELIUM_COOLING_HOLDER = "GATAN_HCHDT_3010_DOUBLE_TILT_HIGH_RESOLUTION_HELIUM_COOLING_HOLDER",
-  GATAN_HCHST_3008_SINGLE_TILT_HIGH_RESOLUTION_HELIUM_COOLING_HOLDER = "GATAN_HCHST_3008_SINGLE_TILT_HIGH_RESOLUTION_HELIUM_COOLING_HOLDER",
-  GATAN_HC_3500_SINGLE_TILT_HEATING_NITROGEN_COOLING_HOLDER = "GATAN_HC_3500_SINGLE_TILT_HEATING_NITROGEN_COOLING_HOLDER",
-  GATAN_HELIUM = "GATAN_HELIUM",
-  GATAN_LIQUID_NITROGEN = "GATAN_LIQUID_NITROGEN",
-  GATAN_UHRST_3500_SINGLE_TILT_ULTRA_HIGH_RESOLUTION_NITROGEN_COOLING_HOLDER = "GATAN_UHRST_3500_SINGLE_TILT_ULTRA_HIGH_RESOLUTION_NITROGEN_COOLING_HOLDER",
-  GATAN_ULTDT_ULTRA_LOW_TEMPERATURE_DOUBLE_TILT_HELIUM_COOLING_HOLDER = "GATAN_ULTDT_ULTRA_LOW_TEMPERATURE_DOUBLE_TILT_HELIUM_COOLING_HOLDER",
-  GATAN_ULTST_ULTRA_LOW_TEMPERATURE_SINGLE_TILT_HELIUM_COOLING_HOLDER = "GATAN_ULTST_ULTRA_LOW_TEMPERATURE_SINGLE_TILT_HELIUM_COOLING_HOLDER",
-  HOME_BUILD = "HOME_BUILD",
-  JEOL = "JEOL",
-  JEOL_3200_FSC_CRYOHOLDER = "JEOL_3200_FSC_CRYOHOLDER",
-  JEOL_CRYOSPECPORTER = "JEOL_CRYOSPECPORTER",
-  OTHER = "OTHER",
-  PHILIPS_ROTATION_HOLDER = "PHILIPS_ROTATION_HOLDER",
-  SIDE_ENTRY_EUCENTRIC = "SIDE_ENTRY_EUCENTRIC"
-}
-
-export enum EmSoftwareCategory {
-  CLASSIFICATION = "CLASSIFICATION",
-  CRYSTALLOGRAPHY_MERGING = "CRYSTALLOGRAPHY_MERGING",
-  CTF_CORRECTION = "CTF_CORRECTION",
-  DIFFRACTION_INDEXING = "DIFFRACTION_INDEXING",
-  FINAL_EULER_ASSIGNMENT = "FINAL_EULER_ASSIGNMENT",
-  IMAGE_ACQUISITION = "IMAGE_ACQUISITION",
-  INITIAL_EULER_ASSIGNMENT = "INITIAL_EULER_ASSIGNMENT",
-  LATTICE_DISTORTION_CORRECTION = "LATTICE_DISTORTION_CORRECTION",
-  LAYERLINE_INDEXING = "LAYERLINE_INDEXING",
-  MASKING = "MASKING",
-  MODEL_FITTING = "MODEL_FITTING",
-  MODEL_REFINEMENT = "MODEL_REFINEMENT",
-  MOLECULAR_REPLACEMENT = "MOLECULAR_REPLACEMENT",
-  OTHER = "OTHER",
-  PARTICLE_SELECTION = "PARTICLE_SELECTION",
-  RECONSTRUCTION = "RECONSTRUCTION",
-  SERIES_ALIGNMENT = "SERIES_ALIGNMENT",
-  SYMMETRY_DETERMINATION = "SYMMETRY_DETERMINATION",
-  VOLUME_SELECTION = "VOLUME_SELECTION"
-}
-
-export enum EmSpecimenEmbeddingApplied {
-  NO = "NO",
-  YES = "YES"
-}
-
-export enum EmSpecimenShadowingApplied {
-  NO = "NO",
-  YES = "YES"
-}
-
-export enum EmSpecimenStainingApplied {
-  NO = "NO",
-  YES = "YES"
-}
-
-export enum EmSpecimenVitrificationApplied {
-  NO = "NO",
-  YES = "YES"
-}
-
-export enum EmStainingType {
-  NEGATIVE = "NEGATIVE",
-  NONE = "NONE",
-  POSITIVE = "POSITIVE"
-}
-
-export enum EmVitrificationCryogenName {
-  ETHANE = "ETHANE",
-  ETHANE_PROPANE = "ETHANE_PROPANE",
-  FREON_12 = "FREON_12",
-  FREON_22 = "FREON_22",
-  HELIUM = "HELIUM",
-  METHANE = "METHANE",
-  NITROGEN = "NITROGEN",
-  OTHER = "OTHER",
-  PROPANE = "PROPANE"
-}
-
-export enum EmVitrificationInstrument {
-  EMS_002_RAPID_IMMERSION_FREEZER = "EMS_002_RAPID_IMMERSION_FREEZER",
-  FEI_VITROBOT_MARK_I = "FEI_VITROBOT_MARK_I",
-  FEI_VITROBOT_MARK_II = "FEI_VITROBOT_MARK_II",
-  FEI_VITROBOT_MARK_III = "FEI_VITROBOT_MARK_III",
-  FEI_VITROBOT_MARK_IV = "FEI_VITROBOT_MARK_IV",
-  GATAN_CRYOPLUNGE_3 = "GATAN_CRYOPLUNGE_3",
-  HOMEMADE_PLUNGER = "HOMEMADE_PLUNGER",
-  LEICA_EM_CPC = "LEICA_EM_CPC",
-  LEICA_EM_GP = "LEICA_EM_GP",
-  LEICA_KF_80 = "LEICA_KF_80",
-  LEICA_PLUNGER = "LEICA_PLUNGER",
-  REICHERT_JUNG_PLUNGER = "REICHERT_JUNG_PLUNGER",
-  SPOTITON = "SPOTITON",
-  ZEISS_PLUNGE_FREEZER_CRYOBOX = "ZEISS_PLUNGE_FREEZER_CRYOBOX"
-}
-
-export enum EntityRcsbMultipleSourceFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum EntitySrcMethod {
-  MAN = "MAN",
-  NAT = "NAT",
-  SYN = "SYN"
-}
-
-export enum EntityType {
-  BRANCHED = "BRANCHED",
-  MACROLIDE = "MACROLIDE",
-  NON_POLYMER = "NON_POLYMER",
-  POLYMER = "POLYMER",
-  WATER = "WATER"
-}
-
-export enum EntityPolyNstdLinkage {
-  N = "N",
-  NO = "NO",
-  Y = "Y",
-  YES = "YES"
-}
-
-export enum EntityPolyNstdMonomer {
-  N = "N",
-  NO = "NO",
-  Y = "Y",
-  YES = "YES"
-}
-
-export enum EntityPolyType {
-  CYCLIC_PSEUDO_PEPTIDE = "CYCLIC_PSEUDO_PEPTIDE",
-  OTHER = "OTHER",
-  PEPTIDE_NUCLEIC_ACID = "PEPTIDE_NUCLEIC_ACID",
-  POLYDEOXYRIBONUCLEOTIDE = "POLYDEOXYRIBONUCLEOTIDE",
-  POLYDEOXYRIBONUCLEOTIDE_POLYRIBONUCLEOTIDE_HYBRID = "POLYDEOXYRIBONUCLEOTIDE_POLYRIBONUCLEOTIDE_HYBRID",
-  POLYPEPTIDE_D = "POLYPEPTIDE_D",
-  POLYPEPTIDE_L = "POLYPEPTIDE_L",
-  POLYRIBONUCLEOTIDE = "POLYRIBONUCLEOTIDE",
-  POLYSACCHARIDE_D = "POLYSACCHARIDE_D",
-  POLYSACCHARIDE_L = "POLYSACCHARIDE_L"
-}
-
-export enum EntitySrcGenPdbxAltSourceFlag {
-  MODEL = "MODEL",
-  SAMPLE = "SAMPLE"
-}
-
-export enum EntitySrcGenPdbxSeqType {
-  BIOLOGICAL_SEQUENCE = "BIOLOGICAL_SEQUENCE",
-  C_TERMINAL_TAG = "C_TERMINAL_TAG",
-  LINKER = "LINKER",
-  N_TERMINAL_TAG = "N_TERMINAL_TAG"
-}
-
-export enum EntitySrcNatPdbxAltSourceFlag {
-  MODEL = "MODEL",
-  SAMPLE = "SAMPLE"
-}
-
-export enum PdbxEntitySrcSynPdbxAltSourceFlag {
-  MODEL = "MODEL",
-  SAMPLE = "SAMPLE"
-}
-
-export enum RcsbEntityHostOrganismProvenanceCode {
-  PDB_PRIMARY_DATA = "PDB_PRIMARY_DATA"
-}
-
-export enum RcsbEntitySourceOrganismProvenanceCode {
-  PDB_PRIMARY_DATA = "PDB_PRIMARY_DATA"
-}
-
-export enum RcsbEntitySourceOrganismSourceType {
-  GENETICALLY_ENGINEERED = "GENETICALLY_ENGINEERED",
-  NATURAL = "NATURAL",
-  SYNTHETIC = "SYNTHETIC"
-}
-
-export enum RcsbMembraneProvenanceSource {
-  HOMOLOGY = "HOMOLOGY",
-  MPSTRUCT = "MPSTRUCT"
-}
-
-export enum StructAsymPdbxBlankPdbChainidFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum FeatureType {
-  ACTIVE_SITE = "ACTIVE_SITE",
-  BINDING_SITE = "BINDING_SITE",
-  CALCIUM_BINDING_REGION = "CALCIUM_BINDING_REGION",
-  CHAIN = "CHAIN",
-  COILED_COIL_REGION = "COILED_COIL_REGION",
-  COMPOSITIONALLY_BIASED_REGION = "COMPOSITIONALLY_BIASED_REGION",
-  CROSS_LINK = "CROSS_LINK",
-  DISULFIDE_BOND = "DISULFIDE_BOND",
-  DNA_BINDING_REGION = "DNA_BINDING_REGION",
-  DOMAIN = "DOMAIN",
-  GLYCOSYLATION_SITE = "GLYCOSYLATION_SITE",
-  HELIX = "HELIX",
-  INITIATOR_METHIONINE = "INITIATOR_METHIONINE",
-  INTRAMEMBRANE_REGION = "INTRAMEMBRANE_REGION",
-  LIPID_MOIETY_BINDING_REGION = "LIPID_MOIETY_BINDING_REGION",
-  METAL_ION_BINDING_SITE = "METAL_ION_BINDING_SITE",
-  MODIFIED_RESIDUE = "MODIFIED_RESIDUE",
-  MUTAGENESIS_SITE = "MUTAGENESIS_SITE",
-  NON_CONSECUTIVE_RESIDUES = "NON_CONSECUTIVE_RESIDUES",
-  NON_STANDARD_AMINO_ACID = "NON_STANDARD_AMINO_ACID",
-  NON_TERMINAL_RESIDUE = "NON_TERMINAL_RESIDUE",
-  NUCLEOTIDE_PHOSPHATE_BINDING_REGION = "NUCLEOTIDE_PHOSPHATE_BINDING_REGION",
-  PEPTIDE = "PEPTIDE",
-  PROPEPTIDE = "PROPEPTIDE",
-  REGION_OF_INTEREST = "REGION_OF_INTEREST",
-  REPEAT = "REPEAT",
-  SEQUENCE_CONFLICT = "SEQUENCE_CONFLICT",
-  SEQUENCE_VARIANT = "SEQUENCE_VARIANT",
-  SHORT_SEQUENCE_MOTIF = "SHORT_SEQUENCE_MOTIF",
-  SIGNAL_PEPTIDE = "SIGNAL_PEPTIDE",
-  SITE = "SITE",
-  SPLICE_VARIANT = "SPLICE_VARIANT",
-  STRAND = "STRAND",
-  TOPOLOGICAL_DOMAIN = "TOPOLOGICAL_DOMAIN",
-  TRANSIT_PEPTIDE = "TRANSIT_PEPTIDE",
-  TRANSMEMBRANE_REGION = "TRANSMEMBRANE_REGION",
-  TURN = "TURN",
-  UNSURE_RESIDUE = "UNSURE_RESIDUE",
-  ZINC_FINGER_REGION = "ZINC_FINGER_REGION"
-}
-
-export enum GeneNameType {
-  ORDERED_LOCUS = "ORDERED_LOCUS",
-  ORF = "ORF",
-  PRIMARY = "PRIMARY",
-  SYNONYM = "SYNONYM"
-}
-
-export enum OrganismNameType {
-  ABBREVIATION = "ABBREVIATION",
-  COMMON = "COMMON",
-  FULL = "FULL",
-  SCIENTIFIC = "SCIENTIFIC",
-  SYNONYM = "SYNONYM"
-}
-
-export enum OrganismHostNameType {
-  ABBREVIATION = "ABBREVIATION",
-  COMMON = "COMMON",
-  FULL = "FULL",
-  SCIENTIFIC = "SCIENTIFIC",
-  SYNONYM = "SYNONYM"
-}
-
-export enum ExptlMethod {
-  ELECTRON_CRYSTALLOGRAPHY = "ELECTRON_CRYSTALLOGRAPHY",
-  ELECTRON_MICROSCOPY = "ELECTRON_MICROSCOPY",
-  EPR = "EPR",
-  FIBER_DIFFRACTION = "FIBER_DIFFRACTION",
-  FLUORESCENCE_TRANSFER = "FLUORESCENCE_TRANSFER",
-  INFRARED_SPECTROSCOPY = "INFRARED_SPECTROSCOPY",
-  NEUTRON_DIFFRACTION = "NEUTRON_DIFFRACTION",
-  POWDER_DIFFRACTION = "POWDER_DIFFRACTION",
-  SOLID_STATE_NMR = "SOLID_STATE_NMR",
-  SOLUTION_NMR = "SOLUTION_NMR",
-  SOLUTION_SCATTERING = "SOLUTION_SCATTERING",
-  THEORETICAL_MODEL = "THEORETICAL_MODEL",
-  X_RAY_DIFFRACTION = "X_RAY_DIFFRACTION"
-}
-
-export enum PdbxSgProjectFullNameOfCenter {
-  ACCELERATED_TECHNOLOGIES_CENTER_FOR_GENE_TO_3_D_STRUCTURE = "ACCELERATED_TECHNOLOGIES_CENTER_FOR_GENE_TO_3_D_STRUCTURE",
-  ASSEMBLY_DYNAMICS_AND_EVOLUTION_OF_CELL_CELL_AND_CELL_MATRIX_ADHESIONS = "ASSEMBLY_DYNAMICS_AND_EVOLUTION_OF_CELL_CELL_AND_CELL_MATRIX_ADHESIONS",
-  ATOMS_TO_ANIMALS_THE_IMMUNE_FUNCTION_NETWORK = "ATOMS_TO_ANIMALS_THE_IMMUNE_FUNCTION_NETWORK",
-  BACTERIAL_TARGETS_AT_IGS_CNRS_FRANCE = "BACTERIAL_TARGETS_AT_IGS_CNRS_FRANCE",
-  BERKELEY_STRUCTURAL_GENOMICS_CENTER = "BERKELEY_STRUCTURAL_GENOMICS_CENTER",
-  CENTER_FOR_EUKARYOTIC_STRUCTURAL_GENOMICS = "CENTER_FOR_EUKARYOTIC_STRUCTURAL_GENOMICS",
-  CENTER_FOR_HIGH_THROUGHPUT_STRUCTURAL_BIOLOGY = "CENTER_FOR_HIGH_THROUGHPUT_STRUCTURAL_BIOLOGY",
-  CENTER_FOR_MEMBRANE_PROTEINS_OF_INFECTIOUS_DISEASES = "CENTER_FOR_MEMBRANE_PROTEINS_OF_INFECTIOUS_DISEASES",
-  CENTER_FOR_STRUCTURAL_GENOMICS_OF_INFECTIOUS_DISEASES = "CENTER_FOR_STRUCTURAL_GENOMICS_OF_INFECTIOUS_DISEASES",
-  CENTER_FOR_STRUCTURES_OF_MEMBRANE_PROTEINS = "CENTER_FOR_STRUCTURES_OF_MEMBRANE_PROTEINS",
-  CENTER_FOR_THE_X_RAY_STRUCTURE_DETERMINATION_OF_HUMAN_TRANSPORTERS = "CENTER_FOR_THE_X_RAY_STRUCTURE_DETERMINATION_OF_HUMAN_TRANSPORTERS",
-  CHAPERONE_ENABLED_STUDIES_OF_EPIGENETIC_REGULATION_ENZYMES = "CHAPERONE_ENABLED_STUDIES_OF_EPIGENETIC_REGULATION_ENZYMES",
-  ENZYME_DISCOVERY_FOR_NATURAL_PRODUCT_BIOSYNTHESIS = "ENZYME_DISCOVERY_FOR_NATURAL_PRODUCT_BIOSYNTHESIS",
-  GPCR_NETWORK = "GPCR_NETWORK",
-  INTEGRATED_CENTER_FOR_STRUCTURE_AND_FUNCTION_INNOVATION = "INTEGRATED_CENTER_FOR_STRUCTURE_AND_FUNCTION_INNOVATION",
-  ISRAEL_STRUCTURAL_PROTEOMICS_CENTER = "ISRAEL_STRUCTURAL_PROTEOMICS_CENTER",
-  JOINT_CENTER_FOR_STRUCTURAL_GENOMICS = "JOINT_CENTER_FOR_STRUCTURAL_GENOMICS",
-  MARSEILLES_STRUCTURAL_GENOMICS_PROGRAM_AFMB = "MARSEILLES_STRUCTURAL_GENOMICS_PROGRAM_AFMB",
-  MEDICAL_STRUCTURAL_GENOMICS_OF_PATHOGENIC_PROTOZOA = "MEDICAL_STRUCTURAL_GENOMICS_OF_PATHOGENIC_PROTOZOA",
-  MEMBRANE_PROTEIN_STRUCTURAL_BIOLOGY_CONSORTIUM = "MEMBRANE_PROTEIN_STRUCTURAL_BIOLOGY_CONSORTIUM",
-  MEMBRANE_PROTEIN_STRUCTURES_BY_SOLUTION_NMR = "MEMBRANE_PROTEIN_STRUCTURES_BY_SOLUTION_NMR",
-  MIDWEST_CENTER_FOR_MACROMOLECULAR_RESEARCH = "MIDWEST_CENTER_FOR_MACROMOLECULAR_RESEARCH",
-  MIDWEST_CENTER_FOR_STRUCTURAL_GENOMICS = "MIDWEST_CENTER_FOR_STRUCTURAL_GENOMICS",
-  MITOCHONDRIAL_PROTEIN_PARTNERSHIP = "MITOCHONDRIAL_PROTEIN_PARTNERSHIP",
-  MONTREAL_KINGSTON_BACTERIAL_STRUCTURAL_GENOMICS_INITIATIVE = "MONTREAL_KINGSTON_BACTERIAL_STRUCTURAL_GENOMICS_INITIATIVE",
-  MYCOBACTERIUM_TUBERCULOSIS_STRUCTURAL_PROTEOMICS_PROJECT = "MYCOBACTERIUM_TUBERCULOSIS_STRUCTURAL_PROTEOMICS_PROJECT",
-  NEW_YORK_CONSORTIUM_ON_MEMBRANE_PROTEIN_STRUCTURE = "NEW_YORK_CONSORTIUM_ON_MEMBRANE_PROTEIN_STRUCTURE",
-  NEW_YORK_SGX_RESEARCH_CENTER_FOR_STRUCTURAL_GENOMICS = "NEW_YORK_SGX_RESEARCH_CENTER_FOR_STRUCTURAL_GENOMICS",
-  NEW_YORK_STRUCTURAL_GENOMICS_RESEARCH_CONSORTIUM = "NEW_YORK_STRUCTURAL_GENOMICS_RESEARCH_CONSORTIUM",
-  NEW_YORK_STRUCTURAL_GENOMI_X_RESEARCH_CONSORTIUM = "NEW_YORK_STRUCTURAL_GENOMI_X_RESEARCH_CONSORTIUM",
-  NORTHEAST_STRUCTURAL_GENOMICS_CONSORTIUM = "NORTHEAST_STRUCTURAL_GENOMICS_CONSORTIUM",
-  NUCLEOCYTOPLASMIC_TRANSPORT_A_TARGET_FOR_CELLULAR_CONTROL = "NUCLEOCYTOPLASMIC_TRANSPORT_A_TARGET_FOR_CELLULAR_CONTROL",
-  ONTARIO_CENTRE_FOR_STRUCTURAL_PROTEOMICS = "ONTARIO_CENTRE_FOR_STRUCTURAL_PROTEOMICS",
-  OXFORD_PROTEIN_PRODUCTION_FACILITY = "OXFORD_PROTEIN_PRODUCTION_FACILITY",
-  PARIS_SUD_YEAST_STRUCTURAL_GENOMICS = "PARIS_SUD_YEAST_STRUCTURAL_GENOMICS",
-  PARTNERSHIP_FOR_NUCLEAR_RECEPTOR_SIGNALING_CODE_BIOLOGY = "PARTNERSHIP_FOR_NUCLEAR_RECEPTOR_SIGNALING_CODE_BIOLOGY",
-  PARTNERSHIP_FOR_STEM_CELL_BIOLOGY = "PARTNERSHIP_FOR_STEM_CELL_BIOLOGY",
-  PARTNERSHIP_FOR_T_CELL_BIOLOGY = "PARTNERSHIP_FOR_T_CELL_BIOLOGY",
-  PROGRAM_FOR_THE_CHARACTERIZATION_OF_SECRETED_EFFECTOR_PROTEINS = "PROGRAM_FOR_THE_CHARACTERIZATION_OF_SECRETED_EFFECTOR_PROTEINS",
-  PROTEIN_STRUCTURE_FACTORY = "PROTEIN_STRUCTURE_FACTORY",
-  RIKEN_STRUCTURAL_GENOMICS_PROTEOMICS_INITIATIVE = "RIKEN_STRUCTURAL_GENOMICS_PROTEOMICS_INITIATIVE",
-  SCOTTISH_STRUCTURAL_PROTEOMICS_FACILITY = "SCOTTISH_STRUCTURAL_PROTEOMICS_FACILITY",
-  SEATTLE_STRUCTURAL_GENOMICS_CENTER_FOR_INFECTIOUS_DISEASE = "SEATTLE_STRUCTURAL_GENOMICS_CENTER_FOR_INFECTIOUS_DISEASE",
-  SOUTHEAST_COLLABORATORY_FOR_STRUCTURAL_GENOMICS = "SOUTHEAST_COLLABORATORY_FOR_STRUCTURAL_GENOMICS",
-  SOUTH_AFRICA_STRUCTURAL_TARGETS_ANNOTATION_DATABASE = "SOUTH_AFRICA_STRUCTURAL_TARGETS_ANNOTATION_DATABASE",
-  STRUCTURAL_GENOMICS_CONSORTIUM = "STRUCTURAL_GENOMICS_CONSORTIUM",
-  STRUCTURAL_GENOMICS_CONSORTIUM_FOR_RESEARCH_ON_GENE_EXPRESSION = "STRUCTURAL_GENOMICS_CONSORTIUM_FOR_RESEARCH_ON_GENE_EXPRESSION",
-  STRUCTURAL_GENOMICS_OF_PATHOGENIC_PROTOZOA_CONSORTIUM = "STRUCTURAL_GENOMICS_OF_PATHOGENIC_PROTOZOA_CONSORTIUM",
-  STRUCTURAL_PROTEOMICS_IN_EUROPE = "STRUCTURAL_PROTEOMICS_IN_EUROPE",
-  STRUCTURAL_PROTEOMICS_IN_EUROPE_2 = "STRUCTURAL_PROTEOMICS_IN_EUROPE_2",
-  STRUCTURES_OF_MTB_PROTEINS_CONFERRING_SUSCEPTIBILITY_TO_KNOWN_MTB_INHIBITORS = "STRUCTURES_OF_MTB_PROTEINS_CONFERRING_SUSCEPTIBILITY_TO_KNOWN_MTB_INHIBITORS",
-  STRUCTURE_2_FUNCTION_PROJECT = "STRUCTURE_2_FUNCTION_PROJECT",
-  STRUCTURE_DYNAMICS_AND_ACTIVATION_MECHANISMS_OF_CHEMOKINE_RECEPTORS = "STRUCTURE_DYNAMICS_AND_ACTIVATION_MECHANISMS_OF_CHEMOKINE_RECEPTORS",
-  STRUCTURE_FUNCTION_ANALYSIS_OF_POLYMORPHIC_CDI_TOXIN_IMMUNITY_PROTEIN_COMPLEXES = "STRUCTURE_FUNCTION_ANALYSIS_OF_POLYMORPHIC_CDI_TOXIN_IMMUNITY_PROTEIN_COMPLEXES",
-  STRUCTURE_FUNCTION_STUDIES_OF_TIGHT_JUNCTION_MEMBRANE_PROTEINS = "STRUCTURE_FUNCTION_STUDIES_OF_TIGHT_JUNCTION_MEMBRANE_PROTEINS",
-  TB_STRUCTURAL_GENOMICS_CONSORTIUM = "TB_STRUCTURAL_GENOMICS_CONSORTIUM",
-  TRANSCONTINENTAL_EM_INITIATIVE_FOR_MEMBRANE_PROTEIN_STRUCTURE = "TRANSCONTINENTAL_EM_INITIATIVE_FOR_MEMBRANE_PROTEIN_STRUCTURE",
-  TRANSMEMBRANE_PROTEIN_CENTER = "TRANSMEMBRANE_PROTEIN_CENTER"
-}
-
-export enum PdbxSgProjectId {
-  _1 = "_1",
-  _10 = "_10",
-  _2 = "_2",
-  _3 = "_3",
-  _4 = "_4",
-  _5 = "_5",
-  _6 = "_6",
-  _7 = "_7",
-  _8 = "_8",
-  _9 = "_9"
-}
-
-export enum PdbxSgProjectInitialOfCenter {
-  ATCG_3_D = "ATCG_3_D",
-  BIGS = "BIGS",
-  BSGC = "BSGC",
-  BSGI = "BSGI",
-  CEBS = "CEBS",
-  CELLMAT = "CELLMAT",
-  CESG = "CESG",
-  CHSAM = "CHSAM",
-  CHTSB = "CHTSB",
-  CSGID = "CSGID",
-  CSMP = "CSMP",
-  GPCR = "GPCR",
-  IFN = "IFN",
-  ISFI = "ISFI",
-  ISPC = "ISPC",
-  JCSG = "JCSG",
-  MCMR = "MCMR",
-  MCSG = "MCSG",
-  MPID = "MPID",
-  MPP = "MPP",
-  MPSBC = "MPSBC",
-  MP_SBY_NMR = "MP_SBY_NMR",
-  MSGP = "MSGP",
-  MSGPP = "MSGPP",
-  MTBI = "MTBI",
-  NAT_PRO = "NAT_PRO",
-  NESG = "NESG",
-  NH_RS = "NH_RS",
-  NPC_XSTALS = "NPC_XSTALS",
-  NYCOMPS = "NYCOMPS",
-  NYSGRC = "NYSGRC",
-  NYSGXRC = "NYSGXRC",
-  OCSP = "OCSP",
-  OPPF = "OPPF",
-  PCSEP = "PCSEP",
-  PSF = "PSF",
-  RSGI = "RSGI",
-  SASTAD = "SASTAD",
-  SECSG = "SECSG",
-  SGC = "SGC",
-  SGCGES = "SGCGES",
-  SGPP = "SGPP",
-  SPINE = "SPINE",
-  SPINE_2 = "SPINE_2",
-  SSGCID = "SSGCID",
-  SSPF = "SSPF",
-  STEMCELL = "STEMCELL",
-  S_2_F = "S_2_F",
-  TBSGC = "TBSGC",
-  TCELL = "TCELL",
-  TEMIMPS = "TEMIMPS",
-  TJMP = "TJMP",
-  TMPC = "TMPC",
-  TRANSPORT_PDB = "TRANSPORT_PDB",
-  UC_4_CDI = "UC_4_CDI",
-  XMTB = "XMTB",
-  YSG = "YSG"
-}
-
-export enum PdbxSgProjectProjectName {
-  ENZYME_FUNCTION_INITIATIVE = "ENZYME_FUNCTION_INITIATIVE",
-  NIAID_NATIONAL_INSTITUTE_OF_ALLERGY_AND_INFECTIOUS_DISEASES = "NIAID_NATIONAL_INSTITUTE_OF_ALLERGY_AND_INFECTIOUS_DISEASES",
-  NPPSFA_NATIONAL_PROJECT_ON_PROTEIN_STRUCTURAL_AND_FUNCTIONAL_ANALYSES = "NPPSFA_NATIONAL_PROJECT_ON_PROTEIN_STRUCTURAL_AND_FUNCTIONAL_ANALYSES",
-  PSI_BIOLOGY = "PSI_BIOLOGY",
-  PSI_PROTEIN_STRUCTURE_INITIATIVE = "PSI_PROTEIN_STRUCTURE_INITIATIVE"
-}
-
-export enum PdbxAuditRevisionDetailsDataContentType {
-  NMR_RESTRAINTS = "NMR_RESTRAINTS",
-  NMR_SHIFTS = "NMR_SHIFTS",
-  STRUCTURE_FACTORS = "STRUCTURE_FACTORS",
-  STRUCTURE_MODEL = "STRUCTURE_MODEL"
-}
-
-export enum PdbxAuditRevisionDetailsProvider {
-  AUTHOR = "AUTHOR",
-  REPOSITORY = "REPOSITORY"
-}
-
-export enum PdbxAuditRevisionDetailsType {
-  COORDINATE_REPLACEMENT = "COORDINATE_REPLACEMENT",
-  INITIAL_RELEASE = "INITIAL_RELEASE",
-  OBSOLETE = "OBSOLETE"
-}
-
-export enum PdbxAuditRevisionHistoryDataContentType {
-  NMR_RESTRAINTS = "NMR_RESTRAINTS",
-  NMR_SHIFTS = "NMR_SHIFTS",
-  STRUCTURE_FACTORS = "STRUCTURE_FACTORS",
-  STRUCTURE_MODEL = "STRUCTURE_MODEL"
-}
-
-export enum PdbxDatabasePdbObsSprId {
-  OBSLTE = "OBSLTE",
-  SPRSDE = "SPRSDE"
-}
-
-export enum PdbxDatabaseStatusSgEntry {
-  N = "N",
-  Y = "Y"
-}
-
-export enum PdbxDatabaseStatusDepositSite {
-  BMRB = "BMRB",
-  BNL = "BNL",
-  NDB = "NDB",
-  PDBE = "PDBE",
-  PDBJ = "PDBJ",
-  RCSB = "RCSB"
-}
-
-export enum PdbxDatabaseStatusMethodsDevelopmentCategory {
-  CAPRI = "CAPRI",
-  CASD_NMR = "CASD_NMR",
-  CASP = "CASP",
-  D_3_R = "D_3_R",
-  FOLD_IT = "FOLD_IT",
-  GPCR_DOCK = "GPCR_DOCK",
-  RNA_PUZZLES = "RNA_PUZZLES"
-}
-
-export enum PdbxDatabaseStatusPdbFormatCompatible {
-  N = "N",
-  Y = "Y"
-}
-
-export enum PdbxDatabaseStatusProcessSite {
-  BNL = "BNL",
-  NDB = "NDB",
-  PDBE = "PDBE",
-  PDBJ = "PDBJ",
-  RCSB = "RCSB"
-}
-
-export enum PdbxDatabaseStatusStatusCode {
-  AUCO = "AUCO",
-  AUTH = "AUTH",
-  BIB = "BIB",
-  DEL = "DEL",
-  HOLD = "HOLD",
-  HPUB = "HPUB",
-  OBS = "OBS",
-  POLC = "POLC",
-  PROC = "PROC",
-  REFI = "REFI",
-  REL = "REL",
-  REPL = "REPL",
-  REV = "REV",
-  RMVD = "RMVD",
-  TRSF = "TRSF",
-  UPD = "UPD",
-  WAIT = "WAIT",
-  WDRN = "WDRN"
-}
-
-export enum PdbxDatabaseStatusStatusCodeCs {
-  AUTH = "AUTH",
-  HOLD = "HOLD",
-  HPUB = "HPUB",
-  OBS = "OBS",
-  POLC = "POLC",
-  PROC = "PROC",
-  REL = "REL",
-  REPL = "REPL",
-  RMVD = "RMVD",
-  WAIT = "WAIT",
-  WDRN = "WDRN"
-}
-
-export enum PdbxDatabaseStatusStatusCodeMr {
-  AUTH = "AUTH",
-  HOLD = "HOLD",
-  HPUB = "HPUB",
-  OBS = "OBS",
-  POLC = "POLC",
-  PROC = "PROC",
-  REL = "REL",
-  REPL = "REPL",
-  RMVD = "RMVD",
-  WAIT = "WAIT",
-  WDRN = "WDRN"
-}
-
-export enum PdbxDatabaseStatusStatusCodeSf {
-  AUTH = "AUTH",
-  HOLD = "HOLD",
-  HPUB = "HPUB",
-  OBS = "OBS",
-  POLC = "POLC",
-  PROC = "PROC",
-  REL = "REL",
-  REPL = "REPL",
-  RMVD = "RMVD",
-  WAIT = "WAIT",
-  WDRN = "WDRN"
-}
-
-export enum PdbxDepositGroupGroupType {
-  CHANGED_STATE = "CHANGED_STATE",
-  GROUND_STATE = "GROUND_STATE",
-  UNDEFINED = "UNDEFINED"
-}
-
-export enum PdbxMoleculeFeaturesClass {
-  ANTAGONIST = "ANTAGONIST",
-  ANTHELMINTIC = "ANTHELMINTIC",
-  ANTIBIOTIC = "ANTIBIOTIC",
-  ANTIBIOTIC_ANTHELMINTIC = "ANTIBIOTIC_ANTHELMINTIC",
-  ANTIBIOTIC_ANTIMICROBIAL = "ANTIBIOTIC_ANTIMICROBIAL",
-  ANTIBIOTIC_ANTINEOPLASTIC = "ANTIBIOTIC_ANTINEOPLASTIC",
-  ANTICANCER = "ANTICANCER",
-  ANTICOAGULANT = "ANTICOAGULANT",
-  ANTICOAGULANT_ANTITHROMBOTIC = "ANTICOAGULANT_ANTITHROMBOTIC",
-  ANTIFUNGAL = "ANTIFUNGAL",
-  ANTIINFLAMMATORY = "ANTIINFLAMMATORY",
-  ANTIMICROBIAL = "ANTIMICROBIAL",
-  ANTIMICROBIAL_ANTIPARASITIC_ANTIBIOTIC = "ANTIMICROBIAL_ANTIPARASITIC_ANTIBIOTIC",
-  ANTIMICROBIAL_ANTIRETROVIRAL = "ANTIMICROBIAL_ANTIRETROVIRAL",
-  ANTIMICROBIAL_ANTITUMOR = "ANTIMICROBIAL_ANTITUMOR",
-  ANTINEOPLASTIC = "ANTINEOPLASTIC",
-  ANTIPARASITIC = "ANTIPARASITIC",
-  ANTIRETROVIRAL = "ANTIRETROVIRAL",
-  ANTITHROMBOTIC = "ANTITHROMBOTIC",
-  ANTITUMOR = "ANTITUMOR",
-  ANTIVIRAL = "ANTIVIRAL",
-  CASPASE_INHIBITOR = "CASPASE_INHIBITOR",
-  CHAPERONE_BINDING = "CHAPERONE_BINDING",
-  ENZYME_INHIBITOR = "ENZYME_INHIBITOR",
-  GROWTH_FACTOR = "GROWTH_FACTOR",
-  IMMUNOSUPPRESSANT = "IMMUNOSUPPRESSANT",
-  INHIBITOR = "INHIBITOR",
-  LANTIBIOTIC = "LANTIBIOTIC",
-  METABOLISM = "METABOLISM",
-  METAL_TRANSPORT = "METAL_TRANSPORT",
-  OXIDATION_REDUCTION = "OXIDATION_REDUCTION",
-  RECEPTOR = "RECEPTOR",
-  THROMBIN_INHIBITOR = "THROMBIN_INHIBITOR",
-  THROMBIN_INHIBITOR_TRYPSIN_INHIBITOR = "THROMBIN_INHIBITOR_TRYPSIN_INHIBITOR",
-  TOXIN = "TOXIN",
-  TRANSPORT_ACTIVATOR = "TRANSPORT_ACTIVATOR",
-  TRYPSIN_INHIBITOR = "TRYPSIN_INHIBITOR",
-  UNKNOWN = "UNKNOWN"
-}
-
-export enum PdbxMoleculeFeaturesType {
-  AMINOGLYCOSIDE = "AMINOGLYCOSIDE",
-  AMINO_ACID = "AMINO_ACID",
-  ANSAMYCIN = "ANSAMYCIN",
-  ANTHRACYCLINE = "ANTHRACYCLINE",
-  ANTHRAQUINONE = "ANTHRAQUINONE",
-  CHALKOPHORE = "CHALKOPHORE",
-  CHALKOPHORE_POLYPEPTIDE = "CHALKOPHORE_POLYPEPTIDE",
-  CHROMOPHORE = "CHROMOPHORE",
-  CYCLIC_DEPSIPEPTIDE = "CYCLIC_DEPSIPEPTIDE",
-  CYCLIC_LIPOPEPTIDE = "CYCLIC_LIPOPEPTIDE",
-  CYCLIC_PEPTIDE = "CYCLIC_PEPTIDE",
-  GLYCOPEPTIDE = "GLYCOPEPTIDE",
-  HETEROCYCLIC = "HETEROCYCLIC",
-  IMINO_SUGAR = "IMINO_SUGAR",
-  KETO_ACID = "KETO_ACID",
-  LIPOGLYCOPEPTIDE = "LIPOGLYCOPEPTIDE",
-  LIPOPEPTIDE = "LIPOPEPTIDE",
-  MACROLIDE = "MACROLIDE",
-  NON_POLYMER = "NON_POLYMER",
-  NUCLEOSIDE = "NUCLEOSIDE",
-  OLIGOPEPTIDE = "OLIGOPEPTIDE",
-  OLIGOSACCHARIDE = "OLIGOSACCHARIDE",
-  PEPTAIBOL = "PEPTAIBOL",
-  PEPTIDE_LIKE = "PEPTIDE_LIKE",
-  POLYCYCLIC = "POLYCYCLIC",
-  POLYPEPTIDE = "POLYPEPTIDE",
-  POLYSACCHARIDE = "POLYSACCHARIDE",
-  QUINOLONE = "QUINOLONE",
-  SIDEROPHORE = "SIDEROPHORE",
-  THIOLACTONE = "THIOLACTONE",
-  THIOPEPTIDE = "THIOPEPTIDE",
-  UNKNOWN = "UNKNOWN"
-}
-
-export enum PdbxNmrExptlSampleState {
-  ANISOTROPIC = "ANISOTROPIC",
-  ISOTROPIC = "ISOTROPIC"
-}
-
-export enum PdbxNmrExptlSampleConditionsIonicStrengthUnits {
-  M = "M",
-  M_M = "M_M",
-  NOT_DEFINED = "NOT_DEFINED"
-}
-
-export enum PdbxNmrExptlSampleConditionsPhUnits {
-  NOT_DEFINED = "NOT_DEFINED",
-  P_D = "P_D",
-  P_H = "P_H",
-  P_H_ = "P_H_"
-}
-
-export enum PdbxNmrExptlSampleConditionsTemperatureUnits {
-  C = "C",
-  K = "K",
-  NOT_DEFINED = "NOT_DEFINED"
-}
-
-export enum PdbxNmrSampleDetailsType {
-  BICELLE = "BICELLE",
-  EMULSION = "EMULSION",
-  FIBER = "FIBER",
-  FIBROUS_PROTEIN = "FIBROUS_PROTEIN",
-  FILAMENTOUS_VIRUS = "FILAMENTOUS_VIRUS",
-  GEL_SOLID = "GEL_SOLID",
-  GEL_SOLUTION = "GEL_SOLUTION",
-  LIPOSOME = "LIPOSOME",
-  LYOPHILIZED_POWDER = "LYOPHILIZED_POWDER",
-  MEMBRANE = "MEMBRANE",
-  MICELLE = "MICELLE",
-  ORIENTED_MEMBRANE_FILM = "ORIENTED_MEMBRANE_FILM",
-  POLYCRYSTALLINE_POWDER = "POLYCRYSTALLINE_POWDER",
-  REVERSE_MICELLE = "REVERSE_MICELLE",
-  SINGLE_CRYSTAL = "SINGLE_CRYSTAL",
-  SOLID = "SOLID",
-  SOLUTION = "SOLUTION"
-}
-
-export enum PdbxSerialCrystallographySampleDeliveryMethod {
-  FIXED_TARGET = "FIXED_TARGET",
-  INJECTION = "INJECTION"
-}
-
-export enum RcsbBindingSource {
-  BINDINGDB = "BINDINGDB",
-  BINDINGMOAD = "BINDINGMOAD",
-  PDBBIND = "PDBBIND"
-}
-
-export enum RefineLsMatrixType {
-  ATOMBLOCK = "ATOMBLOCK",
-  DIAGONAL = "DIAGONAL",
-  FULL = "FULL",
-  FULLCYCLE = "FULLCYCLE",
-  SPARSE = "SPARSE",
-  USERBLOCK = "USERBLOCK"
-}
-
-export enum RefinePdbxTlsResidualAdpFlag {
-  LIKELY_RESIDUAL = "LIKELY_RESIDUAL",
-  UNVERIFIED = "UNVERIFIED"
-}
-
-export enum SoftwareLanguage {
-  ADA = "ADA",
-  ASSEMBLER = "ASSEMBLER",
-  AWK = "AWK",
-  BASIC = "BASIC",
-  C = "C",
-  CSH = "CSH",
-  C_ = "C_",
-  C_C = "C_C",
-  FORTRAN = "FORTRAN",
-  FORTRAN_77 = "FORTRAN_77",
-  FORTRAN_77_ = "FORTRAN_77_",
-  FORTRAN_90 = "FORTRAN_90",
-  JAVA = "JAVA",
-  JAVA_FORTRAN = "JAVA_FORTRAN",
-  KSH = "KSH",
-  OTHER = "OTHER",
-  PASCAL = "PASCAL",
-  PERL = "PERL",
-  PYTHON = "PYTHON",
-  PYTHON_C = "PYTHON_C",
-  SH = "SH",
-  TCL = "TCL"
-}
-
-export enum SoftwareType {
-  FILTER = "FILTER",
-  JIFFY = "JIFFY",
-  LIBRARY = "LIBRARY",
-  OTHER = "OTHER",
-  PACKAGE = "PACKAGE",
-  PROGRAM = "PROGRAM"
-}
-
-export enum StructPdbxCaspFlag {
-  N = "N",
-  Y = "Y"
-}
-
-export enum SymmetryCellSetting {
-  CUBIC = "CUBIC",
-  HEXAGONAL = "HEXAGONAL",
-  MONOCLINIC = "MONOCLINIC",
-  ORTHORHOMBIC = "ORTHORHOMBIC",
-  RHOMBOHEDRAL = "RHOMBOHEDRAL",
-  TETRAGONAL = "TETRAGONAL",
-  TRICLINIC = "TRICLINIC",
-  TRIGONAL = "TRIGONAL"
-}
-
-// ====================================================
-// END: Typescript template
-// ====================================================
+/** Unrepresentable type */
+export type Unrepresentable = any;
 
 // ====================================================
 // Documents
@@ -1199,72 +13,70 @@ export enum SymmetryCellSetting {
 
 export namespace AssemblySymmetry {
   export type Variables = {
-    readonly pdbId: string;
+    pdbId: string;
   };
 
   export type Query = {
-    readonly __typename?: "Query";
+    __typename?: "Query";
 
-    readonly assemblies?: ReadonlyArray<Assemblies | null> | null;
+    assemblies: Maybe<(Maybe<Assemblies>)[]>;
   };
 
   export type Assemblies = {
-    readonly __typename?: "CoreAssembly";
-
-    readonly pdbx_struct_assembly?: PdbxStructAssembly | null;
+    __typename?: "CoreAssembly";
 
-    readonly rcsb_struct_symmetry?: ReadonlyArray<RcsbStructSymmetry | null> | null;
+    pdbx_struct_assembly: Maybe<PdbxStructAssembly>;
 
-    readonly rcsb_struct_symmetry_provenance?: string | null;
+    rcsb_struct_symmetry: Maybe<(Maybe<RcsbStructSymmetry>)[]>;
   };
 
   export type PdbxStructAssembly = {
-    readonly __typename?: "PdbxStructAssembly";
+    __typename?: "PdbxStructAssembly";
 
-    readonly id: string;
+    id: string;
   };
 
   export type RcsbStructSymmetry = {
-    readonly __typename?: "RcsbStructSymmetry";
+    __typename?: "RcsbStructSymmetry";
 
-    readonly clusters: ReadonlyArray<Clusters | null>;
+    clusters: (Maybe<Clusters>)[];
 
-    readonly kind: RcsbStructSymmetryKind;
+    kind: string;
 
-    readonly oligomeric_state: string;
+    oligomeric_state: string;
 
-    readonly rotation_axes?: ReadonlyArray<RotationAxes | null> | null;
+    rotation_axes: Maybe<(Maybe<RotationAxes>)[]>;
 
-    readonly stoichiometry: ReadonlyArray<string | null>;
+    stoichiometry: (Maybe<string>)[];
 
-    readonly symbol: string;
+    symbol: string;
 
-    readonly type: RcsbStructSymmetryType;
+    type: string;
   };
 
   export type Clusters = {
-    readonly __typename?: "RcsbStructSymmetryClusters";
+    __typename?: "RcsbStructSymmetryClusters";
 
-    readonly avg_rmsd?: number | null;
+    avg_rmsd: Maybe<number>;
 
-    readonly members: ReadonlyArray<Members | null>;
+    members: (Maybe<Members>)[];
   };
 
   export type Members = {
-    readonly __typename?: "ClustersMembers";
+    __typename?: "ClustersMembers";
 
-    readonly asym_id: string;
+    asym_id: string;
 
-    readonly pdbx_struct_oper_list_ids?: ReadonlyArray<string | null> | null;
+    pdbx_struct_oper_list_ids: Maybe<(Maybe<string>)[]>;
   };
 
   export type RotationAxes = {
-    readonly __typename?: "RcsbStructSymmetryRotationAxes";
+    __typename?: "RcsbStructSymmetryRotationAxes";
 
-    readonly start: ReadonlyArray<number | null>;
+    start: (Maybe<number>)[];
 
-    readonly end: ReadonlyArray<number | null>;
+    end: (Maybe<number>)[];
 
-    readonly order?: number | null;
+    order: Maybe<number>;
   };
 }
diff --git a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
index a949d4dfc2c6b72672ea3941226e07b13ed73b91..09abfebeb49ec7b35be09744fa09ef1c202cbc30 100644
--- a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
+++ b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts
@@ -65,7 +65,7 @@ export function AssemblySymmetryAxesVisual(): ComplexVisual<AssemblySymmetryAxes
         createGeometry: createAssemblySymmetryAxesMesh,
         createLocationIterator,
         getLoci,
-        mark,
+        eachLocation: eachAxisLocation,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<AssemblySymmetryAxesParams>, currentProps: PD.Values<AssemblySymmetryAxesParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
@@ -93,7 +93,7 @@ function getLoci(pickingId: PickingId, structure: Structure, id: number) {
     return EmptyLoci
 }
 
-function mark(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+function eachAxisLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!isDataLoci(loci) || loci.tag !== 'axes') return false
     const assemblySymmetry = AssemblySymmetry.get(structure.models[0])
diff --git a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
index efe1bb6fba7c3b5ab1453c7080ec3cb4c2f036b7..434187445d789c4f3419c363e4e936b849a2b2b1 100644
--- a/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
+++ b/src/mol-model-props/rcsb/themes/assembly-symmetry-cluster.ts
@@ -47,7 +47,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
 
     const { symmetryId } = props
 
-    if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
+    if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
         const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
 
         const s = assemblySymmetry.db.rcsb_assembly_symmetry
@@ -99,5 +99,5 @@ export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<Asse
     factory: AssemblySymmetryClusterColorTheme,
     getParams: getAssemblySymmetryClusterColorThemeParams,
     defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
 }
\ No newline at end of file
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index 34bb4d4e6fa78f9b3b2b0796147c41f26253dd7d..35cad50ed5ecb596bf791d532455964f921db7af 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -81,25 +81,7 @@ namespace Loci {
         if (loci.kind === 'structure-loci') {
             return Sphere3D.copy(boundingSphere, loci.structure.boundary.sphere)
         } else if (loci.kind === 'element-loci') {
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    pos(elements[OrderedSet.getAt(indices, i)], tempPos);
-                    sphereHelper.includeStep(tempPos);
-                }
-            }
-            sphereHelper.finishedIncludeStep();
-            for (const e of loci.elements) {
-                const { indices } = e;
-                const pos = e.unit.conformation.position;
-                const { elements } = e.unit;
-                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
-                    pos(elements[OrderedSet.getAt(indices, i)], tempPos);
-                    sphereHelper.radiusStep(tempPos);
-                }
-            }
+            return StructureElement.Loci.getBoundary(loci).sphere;
         } else if (loci.kind === 'link-loci') {
             for (const e of loci.links) {
                 e.aUnit.conformation.position(e.aUnit.elements[e.aIndex], tempPos);
diff --git a/src/mol-model/shape/formarts/ply/plyData_to_shape.ts b/src/mol-model/shape/formarts/ply/plyData_to_shape.ts
index 4a3ec2e2860e34adca9a4029e33a00dfb28fe2ab..b93d38dc0879f56545ec88d2d7ce6b0e1da8203d 100644
--- a/src/mol-model/shape/formarts/ply/plyData_to_shape.ts
+++ b/src/mol-model/shape/formarts/ply/plyData_to_shape.ts
@@ -33,7 +33,7 @@ function collectData_for_Shape(parsedData: ply_form): MyData {
         data.colors[i] = Color.fromRgb(colors[faces[4*i+1]*3+0], colors[faces[4*i+1]*3+1], colors[faces[4*i+1]*3+2]);
         data.labels[i] = parsedData.properties[parsedData.propertyCount * faces[4*i+1] + 10].toString();
             //i.toString();
-        data.transforms[i] = 0;
+        // data.transforms[i] = 0;
     }
     console.log('data', data);
     return data;
@@ -60,6 +60,8 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], normals: nu
         addTriangle(builderState, triangle_vertices, triangle_normals, triangle_indices)
     }
     let a = MeshBuilder.getMesh(builderState);
+    // a.normalsComputed = false
+    // Mesh.computeNormalsImmediate(a)
     console.log(a);
     return a
 }
diff --git a/src/mol-model/structure/export/categories/utils.ts b/src/mol-model/structure/export/categories/utils.ts
index d0a63367f8a4a3c7e472b6feaed0ff32316a4beb..4b8acb1e8882de25959bf6410ecb85f8e2df67d5 100644
--- a/src/mol-model/structure/export/categories/utils.ts
+++ b/src/mol-model/structure/export/categories/utils.ts
@@ -5,7 +5,7 @@
  */
 
 import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
-import { unionMany } from 'mol-util/set';
+import { SetUtils } from 'mol-util/set';
 import { Model } from '../../model';
 import { Structure } from '../../structure';
 import { EntityIndex } from '../../model/indexing';
@@ -20,7 +20,7 @@ export function getModelMmCifCategory<K extends keyof mmCIF_Schema>(model: Model
 }
 
 export function getUniqueResidueNamesFromStructures(structures: Structure[]) {
-    return unionMany(structures.map(s => s.uniqueResidueNames));
+    return SetUtils.unionMany(...structures.map(s => s.uniqueResidueNames));
 }
 
 export function getUniqueEntityIdsFromStructures(structures: Structure[]): Set<string> {
diff --git a/src/mol-model/structure/model.ts b/src/mol-model/structure/model.ts
index 05dec32e216ca63c83ad5f5e80767f027fef847b..df9e98d18fa289780ace42f1326250fd232ce92e 100644
--- a/src/mol-model/structure/model.ts
+++ b/src/mol-model/structure/model.ts
@@ -6,10 +6,9 @@
 
 import { Model } from './model/model'
 import * as Types from './model/types'
-import Format from './model/format'
 import { ModelSymmetry } from './model/properties/symmetry'
 import StructureSequence from './model/properties/sequence'
 
 export * from './model/properties/custom'
 export * from './model/indexing'
-export { Model, Types, Format, ModelSymmetry, StructureSequence }
\ No newline at end of file
+export { Model, Types, ModelSymmetry, StructureSequence }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/format.ts b/src/mol-model/structure/model/format.ts
deleted file mode 100644
index d2053f170306e4e704fc6399dda1f0914a276b8e..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/format.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// import { File as GroFile } from 'mol-io/reader/gro/schema'
-import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'
-import CIF, { CifFrame } from 'mol-io/reader/cif';
-
-type Format =
-    // | Format.gro
-    | Format.mmCIF
-
-namespace Format {
-    // export interface gro { kind: 'gro', data: GroFile }
-    export interface mmCIF { kind: 'mmCIF', data: mmCIF_Database, frame: CifFrame }
-
-    export function mmCIF(frame: CifFrame, data?: mmCIF_Database): mmCIF {
-        return { kind: 'mmCIF', data: data || CIF.schema.mmCIF(frame), frame };
-    }
-}
-
-export default Format
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/gro.ts b/src/mol-model/structure/model/formats/gro.ts
deleted file mode 100644
index 1e04fb1e7e065d2c62174e0301d1b693d4f61bca..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/model/formats/gro.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-// TODO: make this work when the time comes.
-// /**
-//  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
-//  *
-//  * @author Alexander Rose <alexander.rose@weirdbyte.de>
-//  */
-
-// import { Column, Table } from 'mol-data/db';
-// import { Interval, Segmentation } from 'mol-data/int';
-// import { mmCIF_Schema as mmCIF } from 'mol-io/reader/cif/schema/mmcif';
-// import { Atoms } from 'mol-io/reader/gro/schema';
-// import UUID from 'mol-util/uuid';
-// import Format from '../format';
-// import Model from '../model';
-// import { AtomicConformation, AtomicData, AtomicSegments, AtomsSchema, ChainsSchema, ResiduesSchema } from '../properties/atomic';
-// import { CoarseHierarchy } from '../properties/coarse';
-// import { Entities } from '../properties/common';
-// import Sequence from '../properties/sequence';
-// import { ModelSymmetry } from '../properties/symmetry';
-// import { guessElement } from '../properties/utils/guess-element';
-// import { getAtomicKeys } from '../properties/utils/keys';
-// import { ElementSymbol } from '../types';
-
-// import gro_Format = Format.gro
-
-// type HierarchyOffsets = { residues: ArrayLike<number>, chains: ArrayLike<number> }
-
-// function findHierarchyOffsets(atomsData: Atoms, bounds: Interval) {
-//     const start = Interval.start(bounds), end = Interval.end(bounds);
-//     const residues = [start], chains = [start];
-
-//     const { residueName, residueNumber } = atomsData;
-
-//     for (let i = start + 1; i < end; i++) {
-//         const newResidue = !residueNumber.areValuesEqual(i - 1, i)
-//             || !residueName.areValuesEqual(i - 1, i);
-//         console.log(residueName.value(i - 1), residueName.value(i), residueNumber.value(i - 1), residueNumber.value(i), newResidue)
-//         if (newResidue) residues[residues.length] = i;
-//     }
-//     console.log(residues, residues.length)
-//     return { residues, chains };
-// }
-
-// function guessElementSymbol (value: string) {
-//     return ElementSymbol(guessElement(value));
-// }
-
-// function createHierarchyData(atomsData: Atoms, offsets: HierarchyOffsets): AtomicData {
-//     console.log(atomsData.atomName)
-//     const atoms = Table.ofColumns(AtomsSchema, {
-//         type_symbol: Column.ofArray({ array: Column.mapToArray(atomsData.atomName, guessElementSymbol), schema: Column.Schema.Aliased<ElementSymbol>(Column.Schema.str) }),
-//         label_atom_id: atomsData.atomName,
-//         auth_atom_id: atomsData.atomName,
-//         label_alt_id: Column.Undefined(atomsData.count, Column.Schema.str),
-//         pdbx_formal_charge: Column.Undefined(atomsData.count, Column.Schema.int)
-//     });
-
-//     const residues = Table.view(Table.ofColumns(ResiduesSchema, {
-//         group_PDB: Column.Undefined(atomsData.count, Column.Schema.Aliased<'ATOM' | 'HETATM'>(Column.Schema.str)),
-//         label_comp_id: atomsData.residueName,
-//         auth_comp_id: atomsData.residueName,
-//         label_seq_id: atomsData.residueNumber,
-//         auth_seq_id: atomsData.residueNumber,
-//         pdbx_PDB_ins_code: Column.Undefined(atomsData.count, Column.Schema.str),
-//     }), ResiduesSchema, offsets.residues);
-//     // Optimize the numeric columns
-//     Table.columnToArray(residues, 'label_seq_id', Int32Array);
-//     Table.columnToArray(residues, 'auth_seq_id', Int32Array);
-
-//     // const chains = Table.ofColumns(Hierarchy.ChainsSchema, {
-//     //     label_asym_id: Column.ofConst('A', atomsData.count, Column.Schema.str),
-//     //     auth_asym_id: Column.ofConst('A', atomsData.count, Column.Schema.str),
-//     //     label_entity_id: Column.Undefined(atomsData.count, Column.Schema.str)
-//     // });
-
-//     const chains = Table.ofUndefinedColumns(ChainsSchema, 0);
-
-//     return { atoms, residues, chains };
-// }
-
-// function getConformation(atoms: Atoms): AtomicConformation {
-//     return {
-//         id: UUID.create(),
-//         atomId: atoms.atomNumber,
-//         occupancy: Column.Undefined(atoms.count, Column.Schema.int),
-//         B_iso_or_equiv: Column.Undefined(atoms.count, Column.Schema.float),
-//         x: Column.mapToArray(atoms.x, x => x * 10, Float32Array),
-//         y: Column.mapToArray(atoms.y, y => y * 10, Float32Array),
-//         z: Column.mapToArray(atoms.z, z => z * 10, Float32Array)
-//     }
-// }
-
-// function isHierarchyDataEqual(a: AtomicData, b: AtomicData) {
-//     // need to cast because of how TS handles type resolution for interfaces https://github.com/Microsoft/TypeScript/issues/15300
-//     return Table.areEqual(a.residues as Table<ResiduesSchema>, b.residues as Table<ResiduesSchema>)
-//         && Table.areEqual(a.atoms as Table<AtomsSchema>, b.atoms as Table<AtomsSchema>)
-// }
-
-// function createModel(format: gro_Format, modelNum: number, previous?: Model): Model {
-//     const structure = format.data.structures[modelNum];
-//     const bounds = Interval.ofBounds(0, structure.atoms.count);
-
-//     const hierarchyOffsets = findHierarchyOffsets(structure.atoms, bounds);
-//     const hierarchyData = createHierarchyData(structure.atoms, hierarchyOffsets);
-
-//     if (previous && isHierarchyDataEqual(previous.atomicHierarchy, hierarchyData)) {
-//         return {
-//             ...previous,
-//             atomicConformation: getConformation(structure.atoms)
-//         };
-//     }
-
-//     const hierarchySegments: AtomicSegments = {
-//         residueSegments: Segmentation.ofOffsets(hierarchyOffsets.residues, bounds),
-//         chainSegments: Segmentation.ofOffsets(hierarchyOffsets.chains, bounds),
-//     }
-
-//     // TODO: create a better mock entity
-//     const entityTable = Table.ofRows<mmCIF['entity']>(mmCIF.entity, [{
-//         id: '0',
-//         src_method: 'syn',
-//         type: 'polymer',
-//         pdbx_number_of_molecules: 1
-//     }]);
-
-//     const entities: Entities = { data: entityTable, getEntityIndex: Column.createIndexer(entityTable.id) };
-
-//     const hierarchyKeys = getAtomicKeys(hierarchyData, entities, hierarchySegments);
-//     const atomicHierarchy = { ...hierarchyData, ...hierarchyKeys, ...hierarchySegments };
-//     return {
-//         id: UUID.create(),
-//         sourceData: format,
-//         modelNum,
-//         atomicHierarchy,
-//         entities,
-//         sequence: Sequence.fromAtomicHierarchy(atomicHierarchy),
-//         atomicConformation: getConformation(structure.atoms),
-//         coarseHierarchy: CoarseHierarchy.Empty,
-//         coarseConformation: void 0 as any,
-//         symmetry: ModelSymmetry.Default
-//     };
-// }
-
-// function buildModels(format: gro_Format): ReadonlyArray<Model> {
-//     const models: Model[] = [];
-
-//     format.data.structures.forEach((_, i) => {
-//         const model = createModel(format, i, models.length > 0 ? models[models.length - 1] : void 0);
-//         models.push(model);
-//     });
-//     return models;
-// }
-
-// export default buildModels;
diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts
index a9442fc948c6ffe8e1de0f19236ec750760e8a68..c777d8d55fe61d75afc5b24f07e58dbfe0b4748e 100644
--- a/src/mol-model/structure/model/model.ts
+++ b/src/mol-model/structure/model/model.ts
@@ -4,19 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import UUID from 'mol-util/uuid'
-import Format from './format'
-import StructureSequence from './properties/sequence'
-import { AtomicHierarchy, AtomicConformation } from './properties/atomic'
-import { ModelSymmetry } from './properties/symmetry'
-import { CoarseHierarchy, CoarseConformation } from './properties/coarse'
+import UUID from 'mol-util/uuid';
+import StructureSequence from './properties/sequence';
+import { AtomicHierarchy, AtomicConformation } from './properties/atomic';
+import { ModelSymmetry } from './properties/symmetry';
+import { CoarseHierarchy, CoarseConformation } from './properties/coarse';
 import { Entities } from './properties/common';
 import { CustomProperties } from './properties/custom';
 import { SecondaryStructure } from './properties/seconday-structure';
-
-import from_mmCIF from './formats/mmcif'
-import { ChemicalComponentMap } from './properties/chemical-component';
 import { SaccharideComponentMap } from '../structure/carbohydrates/constants';
+import { ModelFormat } from 'mol-model-formats/structure/format';
+import { ChemicalComponentMap } from './properties/chemical-component';
 
 /**
  * Interface to the "source data" of the molecule.
@@ -30,7 +28,7 @@ export interface Model extends Readonly<{
     // for IHM, corresponds to ihm_model_list.model_id
     modelNum: number,
 
-    sourceData: Format,
+    sourceData: ModelFormat,
 
     symmetry: ModelSymmetry,
     entities: Entities,
@@ -69,10 +67,6 @@ export interface Model extends Readonly<{
 } { }
 
 export namespace Model {
-    export function create(format: Format) {
-        switch (format.kind) {
-            // case 'gro': return from_gro(format);
-            case 'mmCIF': return from_mmCIF(format);
-        }
-    }
+    // TODO: is this enough?
+    export type Trajectory = ReadonlyArray<Model>
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/atomic/hierarchy.ts b/src/mol-model/structure/model/properties/atomic/hierarchy.ts
index 0b0d3c1e0d68660f718a42ec5f9d4b3846ea5d39..7262fe28a3d88264f02cb185cd580d8eb1d09fee 100644
--- a/src/mol-model/structure/model/properties/atomic/hierarchy.ts
+++ b/src/mol-model/structure/model/properties/atomic/hierarchy.ts
@@ -38,7 +38,14 @@ export const AtomsSchema = {
      * The net integer charge assigned to this atom.
      * This is the formal charge assignment normally found in chemical diagrams.
      */
-    pdbx_formal_charge: mmCIF.atom_site.pdbx_formal_charge
+    pdbx_formal_charge: mmCIF.atom_site.pdbx_formal_charge,
+
+    /**
+     * The index of this atom in the input data.
+     * Required because of sorting of atoms.
+     */
+    sourceIndex: Column.Schema.int
+
     // id, occupancy and B_iso_or_equiv are part of conformation
 };
 
@@ -105,8 +112,8 @@ export interface AtomicData {
 
 export interface AtomicDerivedData {
     readonly residue: {
-        readonly traceElementIndex: ArrayLike<ElementIndex>
-        readonly directionElementIndex: ArrayLike<ElementIndex>
+        readonly traceElementIndex: ArrayLike<ElementIndex | -1>
+        readonly directionElementIndex: ArrayLike<ElementIndex | -1>
         readonly moleculeType: ArrayLike<MoleculeType>
     }
 }
@@ -145,7 +152,7 @@ export interface AtomicIndex {
     /**
      * Index of the 1st occurence of this residue.
      * auth_seq_id is used because label_seq_id is undefined for "ligands" in mmCIF.
-     * @param pdbx_PDB_ins_code Empty string for undefined
+     * @param key.pdbx_PDB_ins_code Empty string for undefined
      * @returns index or -1 if not present.
      */
     findResidue(key: AtomicIndex.ResidueKey): ResidueIndex,
@@ -153,7 +160,7 @@ export interface AtomicIndex {
 
     /**
      * Index of the 1st occurence of this residue.
-     * @param pdbx_PDB_ins_code Empty string for undefined
+     * @param key.pdbx_PDB_ins_code Empty string for undefined
      * @returns index or -1 if not present.
      */
     findResidueAuth(key: AtomicIndex.ResidueAuthKey): ResidueIndex,
@@ -161,7 +168,7 @@ export interface AtomicIndex {
     /**
      * Find the residue index where the spefied residue should be inserted to maintain the ordering (entity_id, asym_id, seq_id, ins_code).
      * Useful for determining ranges for sequence-level annotations.
-     * @param pdbx_PDB_ins_code Empty string for undefined
+     * @param key.pdbx_PDB_ins_code Use empty string for undefined
      */
     findResidueInsertion(key: AtomicIndex.ResidueLabelKey): ResidueIndex,
 
@@ -181,11 +188,16 @@ export interface AtomicIndex {
 
     /**
      * Find element index of an atom on a given residue.
-     * @param key
      * @returns index or -1 if the atom is not present.
      */
     findAtomOnResidue(residueIndex: ResidueIndex, label_atom_id: string, label_alt_id?: string): ElementIndex
 
+    /**
+     * Find element index of any given atom on a given residue.
+     * @returns first found index or -1 if none of the given atoms are present.
+     */
+    findAtomsOnResidue(residueIndex: ResidueIndex, label_atom_ids: Set<string>): ElementIndex
+
     // TODO: add indices that support comp_id?
 }
 
diff --git a/src/mol-model/structure/model/properties/chemical-component.ts b/src/mol-model/structure/model/properties/chemical-component.ts
index 35d34e571790872c8e1a110bc125705d7a4b49c4..15efa28fed123964a6f47efc6555f8343f6514c8 100644
--- a/src/mol-model/structure/model/properties/chemical-component.ts
+++ b/src/mol-model/structure/model/properties/chemical-component.ts
@@ -4,16 +4,10 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { MoleculeType, ComponentType } from '../types'
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { Table } from 'mol-data/db';
 
-export interface ChemicalComponent {
-    id: string
-    type: ComponentType
-    moleculeType: MoleculeType
-    name: string
-    synonyms: string[]
-    formula: string
-    formulaWeight: number
-}
+export type ChemicalComponent = Table.Row<mmCIF_Schema['chem_comp']>
+export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
 
-export type ChemicalComponentMap = ReadonlyMap<string, ChemicalComponent>
\ No newline at end of file
+// TODO add data for common chemical components
\ No newline at end of file
diff --git a/src/mol-model/structure/model/properties/utils/atomic-derived.ts b/src/mol-model/structure/model/properties/utils/atomic-derived.ts
index c998a2513010e4ea9315d3202f148d33b982dfce..80c1d10e59e9bbf13e9cfac3ecb3854dcda7cf6a 100644
--- a/src/mol-model/structure/model/properties/utils/atomic-derived.ts
+++ b/src/mol-model/structure/model/properties/utils/atomic-derived.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>
  */
@@ -8,36 +8,50 @@ import { AtomicData } from '../atomic';
 import { ChemicalComponentMap } from '../chemical-component';
 import { AtomicIndex, AtomicDerivedData } from '../atomic/hierarchy';
 import { ElementIndex, ResidueIndex } from '../../indexing';
-import { MoleculeType } from '../../types';
+import { MoleculeType, getMoleculeType, getComponentType } from '../../types';
 import { getAtomIdForAtomRole } from 'mol-model/structure/util';
 
 export function getAtomicDerivedData(data: AtomicData, index: AtomicIndex, chemicalComponentMap: ChemicalComponentMap): AtomicDerivedData {
-    
     const { label_comp_id, _rowCount: n } = data.residues
 
-    const traceElementIndex = new Uint32Array(n)
-    const directionElementIndex = new Uint32Array(n)
+    const traceElementIndex = new Int32Array(n)
+    const directionElementIndex = new Int32Array(n)
     const moleculeType = new Uint8Array(n)
 
+    const moleculeTypeMap = new Map<string, MoleculeType>()
+
     for (let i = 0; i < n; ++i) {
         const compId = label_comp_id.value(i)
         const chemCompMap = chemicalComponentMap
-        const cc = chemCompMap.get(compId)
-        const molType = cc ? cc.moleculeType : MoleculeType.unknown
+        let molType: MoleculeType
+        if (moleculeTypeMap.has(compId)) {
+            molType = moleculeTypeMap.get(compId)!
+        } else if (chemCompMap.has(compId)) {
+            molType = getMoleculeType(chemCompMap.get(compId)!.type, compId)
+            moleculeTypeMap.set(compId, molType)
+        } else {
+            molType = getMoleculeType(getComponentType(compId), compId)
+            // TODO if unknown molecule type, use atom names to guess molecule type
+            moleculeTypeMap.set(compId, molType)
+        }
         moleculeType[i] = molType
 
         const traceAtomId = getAtomIdForAtomRole(molType, 'trace')
-        traceElementIndex[i] = index.findAtomOnResidue(i as ResidueIndex, traceAtomId)
+        let traceIndex = index.findAtomsOnResidue(i as ResidueIndex, traceAtomId)
+        if (traceIndex === -1) {
+            const coarseAtomId = getAtomIdForAtomRole(molType, 'coarseBackbone')
+            traceIndex = index.findAtomsOnResidue(i as ResidueIndex, coarseAtomId)
+        }
+        traceElementIndex[i] = traceIndex
 
         const directionAtomId = getAtomIdForAtomRole(molType, 'direction')
-        directionElementIndex[i] = index.findAtomOnResidue(i as ResidueIndex, directionAtomId)
+        directionElementIndex[i] = index.findAtomsOnResidue(i as ResidueIndex, directionAtomId)
     }
 
-
     return {
         residue: {
-            traceElementIndex: traceElementIndex as unknown as ArrayLike<ElementIndex>,
-            directionElementIndex: directionElementIndex as unknown as ArrayLike<ElementIndex>,
+            traceElementIndex: traceElementIndex as unknown as ArrayLike<ElementIndex | -1>,
+            directionElementIndex: directionElementIndex as unknown as ArrayLike<ElementIndex | -1>,
             moleculeType: moleculeType as unknown as ArrayLike<MoleculeType>,
         }
     }
diff --git a/src/mol-model/structure/model/properties/utils/atomic-index.ts b/src/mol-model/structure/model/properties/utils/atomic-index.ts
index 9460ed19474e0daf1308069ab345a115bd4042de..17bc9dc6426fd67b758304a87b7930bee0a0dd4f 100644
--- a/src/mol-model/structure/model/properties/utils/atomic-index.ts
+++ b/src/mol-model/structure/model/properties/utils/atomic-index.ts
@@ -158,6 +158,10 @@ class Index implements AtomicIndex {
         return findAtomByNameAndAltLoc(this.residueOffsets[rI], this.residueOffsets[rI + 1], this.map.label_atom_id, this.map.label_alt_id, label_atom_id, label_alt_id);
     }
 
+    findAtomsOnResidue(rI: ResidueIndex, label_atom_ids: Set<string>) {
+        return findAtomByNames(this.residueOffsets[rI], this.residueOffsets[rI + 1], this.map.label_atom_id, label_atom_ids)
+    }
+
     constructor(private map: Mapping) {
         this.entityIndex = map.entities.getEntityIndex;
         this.residueOffsets = this.map.segments.residueAtomSegments.offsets;
@@ -171,6 +175,13 @@ function findAtomByName(start: ElementIndex, end: ElementIndex, data: Column<str
     return -1 as ElementIndex;
 }
 
+function findAtomByNames(start: ElementIndex, end: ElementIndex, data: Column<string>, atomNames: Set<string>): ElementIndex {
+    for (let i = start; i < end; i++) {
+        if (atomNames.has(data.value(i))) return i;
+    }
+    return -1 as ElementIndex;
+}
+
 function findAtomByNameAndAltLoc(start: ElementIndex, end: ElementIndex, nameData: Column<string>, altLocData: Column<string>,
     atomName: string, altLoc: string): ElementIndex {
     for (let i = start; i < end; i++) {
diff --git a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts
index 7853fb37776f4f9abc625c332e08b55883357a20..86ddca7d17eed50bf2166f989a6cd4e6c681a429 100644
--- a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts
+++ b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts
@@ -5,10 +5,9 @@
  */
 
 import { AtomicSegments } from '../atomic';
-import { AtomicData, AtomicRanges } from '../atomic/hierarchy';
+import { AtomicData, AtomicRanges, AtomicIndex } from '../atomic/hierarchy';
 import { Segmentation, Interval } from 'mol-data/int';
 import SortedRanges from 'mol-data/int/sorted-ranges';
-import { ChemicalComponentMap } from '../chemical-component';
 import { MoleculeType, isPolymer } from '../../types';
 import { ElementIndex, ResidueIndex } from '../../indexing';
 import { getAtomIdForAtomRole } from '../../../util';
@@ -17,45 +16,32 @@ import { Vec3 } from 'mol-math/linear-algebra';
 
 // TODO add gaps at the ends of the chains by comparing to the polymer sequence data
 
-function getMoleculeType(compId: string, chemicalComponentMap: ChemicalComponentMap) {
-    const cc = chemicalComponentMap.get(compId)
-    return cc ? cc.moleculeType : MoleculeType.unknown
-}
-
-function getElementIndexForAtomId(rI: ResidueIndex, atomId: string, data: AtomicData, segments: AtomicSegments): ElementIndex {
-    const { offsets } = segments.residueAtomSegments
-    const { label_atom_id } = data.atoms
-    for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) {
-        if (label_atom_id.value(j) === atomId) return j
-    }
-    return offsets[rI]
-}
-
-function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap) {
-    const { label_comp_id } = data.residues
-    const mtStart = getMoleculeType(label_comp_id.value(riStart), chemicalComponentMap)
-    const mtEnd = getMoleculeType(label_comp_id.value(riEnd), chemicalComponentMap)
+function areBackboneConnected(riStart: ResidueIndex, riEnd: ResidueIndex, data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, index: AtomicIndex, moleculeType: ArrayLike<MoleculeType>) {
+    const mtStart = moleculeType[riStart]
+    const mtEnd = moleculeType[riEnd]
     if (!isPolymer(mtStart) || !isPolymer(mtEnd)) return false
 
-    const startId = getAtomIdForAtomRole(mtStart, 'backboneStart')
-    const endId = getAtomIdForAtomRole(mtEnd, 'backboneEnd')
+    let eiStart = index.findAtomsOnResidue(riStart, getAtomIdForAtomRole(mtStart, 'backboneStart'))
+    let eiEnd = index.findAtomsOnResidue(riEnd, getAtomIdForAtomRole(mtEnd, 'backboneEnd'))
 
-    const eiStart = getElementIndexForAtomId(riStart, startId, data, segments)
-    const eiEnd = getElementIndexForAtomId(riEnd, endId, data, segments)
+    if (eiStart === -1 || eiEnd === -1) {
+        eiStart = index.findAtomsOnResidue(riStart, getAtomIdForAtomRole(mtStart, 'coarseBackbone'))
+        eiEnd = index.findAtomsOnResidue(riEnd, getAtomIdForAtomRole(mtEnd, 'coarseBackbone'))
+    }
 
     const { x, y, z } = conformation
     const pStart = Vec3.create(x[eiStart], y[eiStart], z[eiStart])
     const pEnd = Vec3.create(x[eiEnd], y[eiEnd], z[eiEnd])
-    return Vec3.distance(pStart, pEnd) < 10
+    return Vec3.distance(pStart, pEnd) < 10 // TODO better distance check, take into account if protein/nucleic and if coarse
 }
 
-export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, chemicalComponentMap: ChemicalComponentMap): AtomicRanges {
+export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conformation: AtomicConformation, index: AtomicIndex, moleculeType: ArrayLike<MoleculeType>): AtomicRanges {
     const polymerRanges: number[] = []
     const gapRanges: number[] = []
     const cyclicPolymerMap = new Map<ResidueIndex, ResidueIndex>()
     const chainIt = Segmentation.transientSegments(segments.chainAtomSegments, Interval.ofBounds(0, data.atoms._rowCount))
     const residueIt = Segmentation.transientSegments(segments.residueAtomSegments, Interval.ofBounds(0, data.atoms._rowCount))
-    const { label_seq_id, label_comp_id } = data.residues
+    const { label_seq_id } = data.residues
 
     let prevSeqId: number
     let prevStart: number
@@ -72,7 +58,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
 
         const riStart = segments.residueAtomSegments.index[chainSegment.start]
         const riEnd = segments.residueAtomSegments.index[chainSegment.end - 1]
-        if (areBackboneConnected(riStart, riEnd, data, segments, conformation, chemicalComponentMap)) {
+        if (areBackboneConnected(riStart, riEnd, data, segments, conformation, index, moleculeType)) {
             cyclicPolymerMap.set(riStart, riEnd)
             cyclicPolymerMap.set(riEnd, riStart)
         }
@@ -80,9 +66,8 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
         while (residueIt.hasNext) {
             const residueSegment = residueIt.move();
             const residueIndex = residueSegment.index
-            const moleculeType = getMoleculeType(label_comp_id.value(residueIndex), chemicalComponentMap)
             const seqId = label_seq_id.value(residueIndex)
-            if (isPolymer(moleculeType)) {
+            if (isPolymer(moleculeType[residueIndex])) {
                 if (startIndex !== -1) {
                     if (seqId !== prevSeqId + 1) {
                         polymerRanges.push(startIndex, prevEnd - 1)
@@ -93,7 +78,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, conf
                     } else {
                         const riStart = segments.residueAtomSegments.index[residueSegment.start]
                         const riEnd = segments.residueAtomSegments.index[prevEnd - 1]
-                        if (!areBackboneConnected(riStart, riEnd, data, segments, conformation, chemicalComponentMap)) {
+                        if (!areBackboneConnected(riStart, riEnd, data, segments, conformation, index, moleculeType)) {
                             polymerRanges.push(startIndex, prevEnd - 1)
                             startIndex = residueSegment.start
                         }
diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.ts b/src/mol-model/structure/model/properties/utils/secondary-structure.ts
new file mode 100644
index 0000000000000000000000000000000000000000..afa8199475f42a616da01e9c8ec056a416c0fd53
--- /dev/null
+++ b/src/mol-model/structure/model/properties/utils/secondary-structure.ts
@@ -0,0 +1,473 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure';
+import { ResidueIndex } from 'mol-model/structure';
+import { MoleculeType, SecondaryStructureType } from 'mol-model/structure/model/types';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { GridLookup3D } from 'mol-math/geometry';
+import { SortedArray } from 'mol-data/int';
+import { IntAdjacencyGraph } from 'mol-math/graph';
+import { BitFlags } from 'mol-util';
+import { ElementIndex } from 'mol-model/structure/model/indexing';
+import { AtomicHierarchy, AtomicConformation } from '../atomic';
+
+export function computeSecondaryStructure(hierarchy: AtomicHierarchy, conformation: AtomicConformation): SecondaryStructure {
+    // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
+    return computeModelDSSP(hierarchy, conformation)
+}
+
+export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
+    const { lookup3d, proteinResidues } = calcAtomicTraceLookup3D(hierarchy, conformation)
+    const backboneIndices = calcBackboneAtomIndices(hierarchy, proteinResidues)
+    const hbonds = calcBackboneHbonds(hierarchy, conformation, proteinResidues, backboneIndices, lookup3d)
+
+    const residueCount = proteinResidues.length
+    const flags = new Uint32Array(residueCount)
+
+    const ctx: DSSPContext = {
+        hierarchy,
+        proteinResidues,
+        flags,
+        hbonds
+    }
+
+    assignBends(ctx)
+    assignTurns(ctx)
+    assignHelices(ctx)
+    assignBridges(ctx)
+    assignLadders(ctx)
+    assignSheets(ctx)
+
+    const assignment = getDSSPAssignment(flags)
+
+    const type = new Uint32Array(hierarchy.residues._rowCount) as unknown as SecondaryStructureType[]
+    for (let i = 0, il = proteinResidues.length; i < il; ++i) {
+        type[proteinResidues[i]] = assignment[i]
+    }
+
+    const secondaryStructure: SecondaryStructure = {
+        type,
+        key: [], // TODO
+        elements: [] // TODO
+    }
+    return secondaryStructure
+}
+
+interface DSSPContext {
+    hierarchy: AtomicHierarchy
+    proteinResidues: SortedArray<ResidueIndex>
+    /** flags for each residue */
+    flags: Uint32Array
+
+    hbonds: DsspHbonds
+}
+
+type DSSPType = BitFlags<DSSPType.Flag>
+namespace DSSPType {
+    export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has
+    export const create: (f: Flag) => DSSPType = BitFlags.create
+    export const enum Flag {
+        _ = 0x0,
+        H = 0x1,
+        B = 0x2,
+        E = 0x4,
+        G = 0x8,
+        I = 0x10,
+        S = 0x20,
+        T = 0x40,
+        T3 = 0x80,
+        T4 = 0x100,
+        T5 = 0x200,
+    }
+}
+
+/** max distance between two C-alpha atoms to check for hbond */
+const caMaxDist = 7.0;
+
+/** min distance between two C-alpha atoms to check for hbond */
+const caMinDist = 4.0;
+
+function calcAtomicTraceLookup3D(hierarchy: AtomicHierarchy, conformation: AtomicConformation) {
+    const { x, y, z } = conformation;
+    const { moleculeType, traceElementIndex } = hierarchy.derived.residue
+    const indices: number[] = []
+    const _proteinResidues: number[] = []
+    for (let i = 0, il = moleculeType.length; i < il; ++i) {
+        if (moleculeType[i] === MoleculeType.protein) {
+            indices[indices.length] = traceElementIndex[i]
+            _proteinResidues[_proteinResidues.length] = i
+        }
+    }
+    const lookup3d = GridLookup3D({ x, y, z, indices: SortedArray.ofSortedArray(indices) }, 4);
+    const proteinResidues = SortedArray.ofSortedArray<ResidueIndex>(_proteinResidues)
+    return { lookup3d, proteinResidues }
+}
+
+interface BackboneAtomIndices {
+    cIndices: ArrayLike<ElementIndex | -1>
+    hIndices: ArrayLike<ElementIndex | -1>
+    oIndices: ArrayLike<ElementIndex | -1>
+    nIndices: ArrayLike<ElementIndex | -1>
+}
+
+function calcBackboneAtomIndices(hierarchy: AtomicHierarchy, proteinResidues: SortedArray<ResidueIndex>): BackboneAtomIndices {
+    const residueCount = proteinResidues.length
+    const { index } = hierarchy
+
+    const c = new Int32Array(residueCount)
+    const h = new Int32Array(residueCount)
+    const o = new Int32Array(residueCount)
+    const n = new Int32Array(residueCount)
+
+    for (let i = 0, il = residueCount; i < il; ++i) {
+        const rI = proteinResidues[i]
+        c[i] = index.findAtomOnResidue(rI, 'C')
+        h[i] = index.findAtomOnResidue(rI, 'H')
+        o[i] = index.findAtomOnResidue(rI, 'O')
+        n[i] = index.findAtomOnResidue(rI, 'N')
+    }
+
+    return {
+        cIndices: c as unknown as ArrayLike<ElementIndex | -1>,
+        hIndices: h as unknown as ArrayLike<ElementIndex | -1>,
+        oIndices: o as unknown as ArrayLike<ElementIndex | -1>,
+        nIndices: n as unknown as ArrayLike<ElementIndex | -1>,
+    }
+}
+
+type DsspHbonds = IntAdjacencyGraph<{ readonly energies: ArrayLike<number> }>
+
+function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices, lookup3d: GridLookup3D): DsspHbonds {
+    const { cIndices, hIndices, nIndices, oIndices } = backboneIndices
+    const { index } = hierarchy
+    const { x, y, z } = conformation
+    const { traceElementIndex } = hierarchy.derived.residue
+
+    const residueCount = proteinResidues.length
+    const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i])
+
+    const oAtomResidues: number[] = [];
+    const nAtomResidues: number[] = [];
+    const energies: number[] = [];
+
+    const oPos = Vec3.zero()
+    const cPos = Vec3.zero()
+    const caPos = Vec3.zero()
+    const nPos = Vec3.zero()
+    const hPos = Vec3.zero()
+
+    const cPosPrev = Vec3.zero()
+    const oPosPrev = Vec3.zero()
+
+    const caMinDistSq = caMinDist * caMinDist
+
+    for (let i = 0, il = proteinResidues.length; i < il; ++i) {
+        const oPI = i
+        const oRI = proteinResidues[i]
+
+        const oAtom = oIndices[oPI]
+        const cAtom = cIndices[oPI]
+        const caAtom = traceElementIndex[oRI]
+
+        // continue if residue is missing O or C atom
+        if (oAtom === -1 || cAtom === -1) continue
+
+        // ignore C-terminal residue as acceptor
+        if (index.findAtomOnResidue(oRI, 'OXT') !== -1) continue
+
+        position(oAtom, oPos)
+        position(cAtom, cPos)
+        position(caAtom, caPos)
+
+        const { indices, count, squaredDistances } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist)
+
+        for (let j = 0; j < count; ++j) {
+            if (squaredDistances[j] < caMinDistSq) continue
+
+            const nPI = indices[j]
+
+            // ignore bonds within a residue or to prev or next residue, TODO take chain border into account
+            if (nPI === oPI || nPI - 1 === oPI || nPI + 1 === oPI) continue
+
+            const nAtom = nIndices[nPI]
+            if (nAtom === -1) continue
+
+            position(nAtom, nPos)
+
+            const hAtom = hIndices[nPI]
+            if (hAtom === -1) {
+                // approximate calculation of H position, TODO factor out
+                if (nPI === 0) continue
+                const nPIprev = nPI - 1
+
+                const oAtomPrev = oIndices[nPIprev]
+                const cAtomPrev = cIndices[nPIprev]
+                if (oAtomPrev === -1 || cAtomPrev === -1) continue
+
+                position(oAtomPrev, oPosPrev)
+                position(cAtomPrev, cPosPrev)
+
+                Vec3.sub(hPos, cPosPrev, oPosPrev)
+                const dist = Vec3.distance(oPosPrev, cPosPrev)
+                Vec3.scaleAndAdd(hPos, nPos, hPos, 1 / dist)
+            } else {
+                position(hAtom, hPos)
+            }
+
+            const e = calcHbondEnergy(oPos, cPos, nPos, hPos)
+            if (e > hbondEnergyCutoff) continue
+
+            oAtomResidues[oAtomResidues.length] = oPI
+            nAtomResidues[nAtomResidues.length] = nPI
+            energies[energies.length] = e
+        }
+    }
+
+    return buildHbondGraph(residueCount, oAtomResidues, nAtomResidues, energies);
+}
+
+function buildHbondGraph(residueCount: number, oAtomResidues: number[], nAtomResidues: number[], energies: number[]) {
+    const builder = new IntAdjacencyGraph.DirectedEdgeBuilder(residueCount, oAtomResidues, nAtomResidues);
+    const _energies = new Float32Array(builder.slotCount);
+
+    for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
+        builder.addNextEdge();
+        builder.assignProperty(_energies, energies[i]);
+    }
+
+    return builder.createGraph({ energies });
+}
+
+/** Original priority: H,B,E,G,I,T,S */
+function getOriginalResidueFlag(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
+    if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
+    if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
+    if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
+    if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
+    return SecondaryStructureType.Flag.None
+}
+
+/** Version 2.1.0 priority: I,H,B,E,G,T,S */
+function getUpdatedResidueFlag(f: DSSPType) {
+    if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I
+    if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H
+    if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B
+    if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E
+    if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G
+    if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T
+    if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S
+    return SecondaryStructureType.Flag.None
+}
+
+// function geFlagName(f: DSSPType) {
+//     if (DSSPType.is(f, DSSPType.Flag.I)) return 'I'
+//     if (DSSPType.is(f, DSSPType.Flag.H)) return 'H'
+//     if (DSSPType.is(f, DSSPType.Flag.B)) return 'B'
+//     if (DSSPType.is(f, DSSPType.Flag.E)) return 'E'
+//     if (DSSPType.is(f, DSSPType.Flag.G)) return 'G'
+//     if (DSSPType.is(f, DSSPType.Flag.T)) return 'T'
+//     if (DSSPType.is(f, DSSPType.Flag.S)) return 'S'
+//     return '-'
+// }
+
+function getDSSPAssignment(flags: Uint32Array, useOriginal = false) {
+    const getResidueFlag = useOriginal ? getOriginalResidueFlag : getUpdatedResidueFlag
+    const type = new Uint32Array(flags.length)
+    for (let i = 0, il = flags.length; i < il; ++i) {
+        const f = DSSPType.create(flags[i])
+        // console.log(i, geFlagName(f))
+        type[i] = getResidueFlag(f)
+    }
+    return type as unknown as ArrayLike<SecondaryStructureType>
+}
+
+/**
+ * Constant for electrostatic energy in kcal/mol
+ *      f  *  q1 *   q2
+ * Q = -332 * 0.42 * 0.20
+ *
+ * f is the dimensional factor
+ *
+ * q1 and q2 are partial charges which are placed on the C,O
+ * (+q1,-q1) and N,H (-q2,+q2)
+ */
+const Q = -27.888
+
+/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */
+const hbondEnergyCutoff = -0.5
+
+/**
+ * E = Q * (1/r(ON) + l/r(CH) - l/r(OH) - l/r(CN))
+ */
+function calcHbondEnergy(oPos: Vec3, cPos: Vec3, nPos: Vec3, hPos: Vec3) {
+    const distOH = Vec3.distance(oPos, hPos)
+    const distCH = Vec3.distance(cPos, hPos)
+    const distCN = Vec3.distance(cPos, nPos)
+    const distON = Vec3.distance(oPos, nPos)
+
+    const e1 = Q / distOH - Q / distCH
+    const e2 = Q / distCN - Q / distON
+    return e1 + e2
+}
+
+/**
+ * The basic turn pattern is a single H bond of type (i, i + n).
+ * We assign an n-turn at residue i if there is an H bond from CO(i) to NH(i + n),
+ * i.e., “n-turn(i)=: Hbond(i, i + n), n = 3, 4, 5.”
+ *
+ * Type: T
+ */
+function assignTurns(ctx: DSSPContext) {
+    const { proteinResidues, hbonds, flags, hierarchy } = ctx
+    const { chains, residueAtomSegments, chainAtomSegments } = hierarchy
+    const { label_asym_id } = chains
+
+    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+
+    for (let i = 0, il = proteinResidues.length; i < il; ++i) {
+        const rI = proteinResidues[i]
+        const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
+
+        // TODO should take sequence gaps into account
+        for (let k = 3; k <= 5; ++k) {
+            if (i + k >= proteinResidues.length) continue
+
+            const rN = proteinResidues[i + k]
+            const cN = chainAtomSegments.index[residueAtomSegments.offsets[rN]]
+            // check if on same chain
+            if (!label_asym_id.areValuesEqual(cI, cN)) continue
+
+            // check if hbond exists
+            if (hbonds.getDirectedEdgeIndex(i, i + k) !== -1) {
+                flags[i] |= turnFlag[k] | DSSPType.Flag.T
+            }
+        }
+    }
+}
+
+/**
+ * Two nonoverlapping stretches of three residues each, i - 1, i, i + 1 and j - 1, j, j + 1,
+ * form either a parallel or antiparallel bridge, depending on which of
+ * two basic patterns is matched. We assign a bridge between residues i and j
+ * if there are two H bonds characteristic of P-structure; in particular,
+ *
+ * Parallel Bridge(i, j) =:
+ *      [Hbond(i - 1, j) and Hbond(j, i + 1)] or
+ *      [Hbond(j - 1, i) and Hbond(i, j + 1)]
+ *
+ * Antiparallel Bridge(i, j) =:
+ *      [Hbond(i, j) and Hbond(j, i)] or
+ *      [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
+ *
+ * Type: B
+ */
+function assignBridges(ctx: DSSPContext) {
+    const { proteinResidues, hbonds, flags } = ctx
+
+    const { offset, b } = hbonds
+    let i: number, j: number
+
+    for (let k = 0, kl = proteinResidues.length; k < kl; ++k) {
+        for (let t = offset[k], _t = offset[k + 1]; t < _t; t++) {
+            const l = b[t]
+            if (k > l) continue
+
+            // Parallel Bridge(i, j) =: [Hbond(i - 1, j) and Hbond(j, i + 1)]
+            i = k + 1 // k is i - 1
+            j = l
+            if (i !== j && hbonds.getDirectedEdgeIndex(j, i + 1) !== -1) {
+                flags[i] |= DSSPType.Flag.B
+                flags[j] |= DSSPType.Flag.B
+            }
+
+            // Parallel Bridge(i, j) =: [Hbond(j - 1, i) and Hbond(i, j + 1)]
+            i = k
+            j = l - 1 // l is j + 1
+            if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i) !== -1) {
+                flags[i] |= DSSPType.Flag.B
+                flags[j] |= DSSPType.Flag.B
+            }
+
+            // Antiparallel Bridge(i, j) =: [Hbond(i, j) and Hbond(j, i)]
+            i = k
+            j = l
+            if (i !== j && hbonds.getDirectedEdgeIndex(j, i) !== -1) {
+                flags[i] |= DSSPType.Flag.B
+                flags[j] |= DSSPType.Flag.B
+            }
+
+            // Antiparallel Bridge(i, j) =: [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)]
+            i = k + 1
+            j = l - 1
+            if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i + 1) !== -1) {
+                flags[i] |= DSSPType.Flag.B
+                flags[j] |= DSSPType.Flag.B
+            }
+        }
+    }
+}
+
+/**
+ * A minimal helix is defined by two consecutive n-turns.
+ * For example, a 4-helix, of minimal length 4 from residues i to i + 3,
+ * requires 4-turns at residues i - 1 and i,
+ *
+ *      3-helix(i,i + 2)=: [3-turn(i - 1) and 3-turn(i)]
+ *      4-helix(i,i + 3)=: [4-turn(i - 1) and 4-turn(i)]
+ *      5-helix(i,i + 4)=: [5-turn(i - 1) and 5-turn(i)]
+ *
+ * Type: G (n=3), H (n=4), I (n=5)
+ */
+function assignHelices(ctx: DSSPContext) {
+    const { proteinResidues, flags } = ctx
+
+    const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5]
+    const helixFlag = [0, 0, 0, DSSPType.Flag.G, DSSPType.Flag.H, DSSPType.Flag.I]
+
+    for (let i = 1, il = proteinResidues.length; i < il; ++i) {
+        const fI = DSSPType.create(flags[i])
+        const fI1 = DSSPType.create(flags[i - 1])
+
+        for (let k = 3; k <= 5; ++k) {
+            if (DSSPType.is(fI, turnFlag[k]) && DSSPType.is(fI1, turnFlag[k])) {
+                for (let l = 0; l < k; ++l) {
+                    flags[i + l] |= helixFlag[k]
+                }
+            }
+        }
+    }
+}
+
+/**
+ * ladder=: set of one or more consecutive bridges of identical type
+ *
+ * Type: E
+ */
+function assignLadders(ctx: DSSPContext) {
+    // TODO
+}
+
+/**
+ * sheet=: set of one or more ladders connected by shared residues
+ *
+ * Type: E
+ */
+function assignSheets(ctx: DSSPContext) {
+    // TODO
+}
+
+/**
+ * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"]
+ *
+ * Type: S
+ */
+function assignBends(ctx: DSSPContext) {
+    // TODO
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts
index e3c42bba60083bbc84a30486c04988d23e0ff13c..89293da9605b464d3eec6efe7ec3f9d136258b0e 100644
--- a/src/mol-model/structure/model/types.ts
+++ b/src/mol-model/structure/model/types.ts
@@ -1,11 +1,14 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-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 BitFlags from 'mol-util/bit-flags'
+import { SaccharideCompIdMap } from '../structure/carbohydrates/constants';
+import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif';
+import { SetUtils } from 'mol-util/set';
 
 const _esCache = (function () {
     const cache = Object.create(null);
@@ -56,80 +59,61 @@ export const enum MoleculeType {
     saccharide
 }
 
-export type AtomRole = 'trace' | 'direction' | 'backboneStart' | 'backboneEnd'
+export type AtomRole = 'trace' | 'direction' | 'backboneStart' | 'backboneEnd' | 'coarseBackbone'
 
-export const MoleculeTypeAtomRoleId: { [k: number]: { [k in AtomRole]: string } } = {
+export const MoleculeTypeAtomRoleId: { [k: number]: { [k in AtomRole]: Set<string> } } = {
     [MoleculeType.protein]: {
-        trace: 'CA', // TODO 'BB'
-        direction: 'O', // TODO 'OC1', 'O1', 'OX1', 'OXT'
-        backboneStart: 'N',
-        backboneEnd: 'C'
+        trace: new Set(['CA']),
+        direction: new Set(['O', 'OC1', 'O1', 'OX1', 'OXT']),
+        backboneStart: new Set(['N']),
+        backboneEnd: new Set(['C']),
+        coarseBackbone: new Set(['CA', 'BB'])
     },
     [MoleculeType.RNA]: {
-        trace: 'C4\'', // TODO 'C4*'
-        direction: 'C3\'', // 'C3*'
-        backboneStart: 'P',
-        backboneEnd: 'O3\'' // TODO 'O3*'
+        trace: new Set(['C4\'', 'C4*']),
+        direction: new Set(['C3\'', 'C3*']),
+        backboneStart: new Set(['P']),
+        backboneEnd: new Set(['O3\'', 'O3*']),
+        coarseBackbone: new Set(['P'])
     },
     [MoleculeType.DNA]: {
-        trace: 'C3\'', // TODO 'C3*'
-        direction: 'C1\'', // TODO 'C1*'
-        backboneStart: 'P',
-        backboneEnd: 'O3\'' // TODO 'O3*'
+        trace: new Set(['C3\'', 'C3*']),
+        direction: new Set(['C1\'', 'C1*']),
+        backboneStart: new Set(['P']),
+        backboneEnd: new Set(['O3\'', 'O3*']),
+        coarseBackbone: new Set(['P'])
     },
     [MoleculeType.PNA]: {
-        trace: 'N4\'', // TODO 'N4*'
-        direction: 'C7\'', // TODO 'C7*'
-        backboneStart: 'N1\'', // TODO 'N1*'
-        backboneEnd: 'C1\'' // TODO 'C1*'
+        trace: new Set(['N4\'', 'N4*']),
+        direction: new Set(['C7\'', 'C7*']),
+        backboneStart: new Set(['N1\'', 'N1*']),
+        backboneEnd: new Set(['C\'', 'C*']),
+        coarseBackbone: new Set(['P'])
     }
 }
 
-export const ProteinBackboneAtoms = [
+export const ProteinBackboneAtoms = new Set([
     'CA', 'C', 'N', 'O',
     'O1', 'O2', 'OC1', 'OC2', 'OX1', 'OXT',
     'H', 'H1', 'H2', 'H3', 'HA', 'HN',
     'BB'
-]
+])
 
-export const NucleicBackboneAtoms = [
+export const NucleicBackboneAtoms = new Set([
     'P', 'OP1', 'OP2', 'HOP2', 'HOP3',
     'O2\'', 'O3\'', 'O4\'', 'O5\'', 'C1\'', 'C2\'', 'C3\'', 'C4\'', 'C5\'',
     'H1\'', 'H2\'', 'H2\'\'', 'HO2\'', 'H3\'', 'H4\'', 'H5\'', 'H5\'\'', 'HO3\'', 'HO5\'',
     'O2*', 'O3*', 'O4*', 'O5*', 'C1*', 'C2*', 'C3*', 'C4*', 'C5*'
-]
-
-/** Chemical component types as defined in the mmCIF CCD */
-export enum ComponentType {
-    // protein
-    'D-peptide linking', 'L-peptide linking', 'D-peptide NH3 amino terminus',
-    'L-peptide NH3 amino terminus', 'D-peptide COOH carboxy terminus',
-    'L-peptide COOH carboxy terminus', 'peptide linking', 'peptide-like',
-    'L-gamma-peptide, C-delta linking', 'D-gamma-peptide, C-delta linking',
-    'L-beta-peptide, C-gamma linking', 'D-beta-peptide, C-gamma linking',
-
-    // DNA
-    'DNA linking', 'L-DNA linking', 'DNA OH 5 prime terminus', 'DNA OH 3 prime terminus',
-
-    // RNA
-    'RNA linking', 'L-RNA linking', 'RNA OH 5 prime terminus', 'RNA OH 3 prime terminus',
-
-    // sacharide
-    'D-saccharide 1,4 and 1,4 linking', 'L-saccharide 1,4 and 1,4 linking',
-    'D-saccharide 1,4 and 1,6 linking', 'L-saccharide 1,4 and 1,6 linking', 'L-saccharide',
-    'D-saccharide', 'saccharide',
-
-    'non-polymer', 'other'
-}
+])
 
 /** Chemical component type names for protein */
-export const ProteinComponentTypeNames = [
+export const ProteinComponentTypeNames = new Set([
     'D-PEPTIDE LINKING', 'L-PEPTIDE LINKING', 'D-PEPTIDE NH3 AMINO TERMINUS',
     'L-PEPTIDE NH3 AMINO TERMINUS', 'D-PEPTIDE COOH CARBOXY TERMINUS',
     'L-PEPTIDE COOH CARBOXY TERMINUS', 'PEPTIDE LINKING', 'PEPTIDE-LIKE',
     'L-GAMMA-PEPTIDE, C-DELTA LINKING', 'D-GAMMA-PEPTIDE, C-DELTA LINKING',
     'L-BETA-PEPTIDE, C-GAMMA LINKING', 'D-BETA-PEPTIDE, C-GAMMA LINKING',
-]
+])
 
 /** Chemical component type names for DNA */
 export const DNAComponentTypeNames = [
@@ -137,59 +121,104 @@ export const DNAComponentTypeNames = [
 ]
 
 /** Chemical component type names for RNA */
-export const RNAComponentTypeNames = [
+export const RNAComponentTypeNames = new Set([
     'RNA LINKING', 'L-RNA LINKING', 'RNA OH 5 PRIME TERMINUS', 'RNA OH 3 PRIME TERMINUS',
-]
+])
 
 /** Chemical component type names for saccharide */
-export const SaccharideComponentTypeNames = [
+export const SaccharideComponentTypeNames = new Set([
     'D-SACCHARIDE 1,4 AND 1,4 LINKING', 'L-SACCHARIDE 1,4 AND 1,4 LINKING',
     'D-SACCHARIDE 1,4 AND 1,6 LINKING', 'L-SACCHARIDE 1,4 AND 1,6 LINKING', 'L-SACCHARIDE',
     'D-SACCHARIDE', 'SACCHARIDE',
-]
+])
+
+/** Chemical component type names for other */
+export const OtherComponentTypeNames = new Set([
+    'NON-POLYMER', 'OTHER'
+])
 
 /** Common names for water molecules */
-export const WaterNames = [
+export const WaterNames = new Set([
     'SOL', 'WAT', 'HOH', 'H2O', 'W', 'DOD', 'D3O', 'TIP3', 'TIP4', 'SPC'
-]
-
-export const ExtraSaccharideNames = [
-    'MLR'
-]
-
-export const RnaBaseNames = [ 'A', 'C', 'T', 'G', 'I', 'U' ]
-export const DnaBaseNames = [ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ]
-export const PeptideBaseNames = [ 'APN', 'CPN', 'TPN', 'GPN' ]
-export const PurinBaseNames = [ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ]
-export const PyrimidineBaseNames = [ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ]
-export const BaseNames = RnaBaseNames.concat(DnaBaseNames, PeptideBaseNames)
-
-export const isPurinBase = (compId: string) => PurinBaseNames.includes(compId.toUpperCase())
-export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.includes(compId.toUpperCase())
+])
+
+export const AminoAcidNames = new Set([
+    'HIS', 'ARG', 'LYS', 'ILE', 'PHE', 'LEU', 'TRP', 'ALA', 'MET', 'PRO', 'CYS',
+    'ASN', 'VAL', 'GLY', 'SER', 'GLN', 'TYR', 'ASP', 'GLU', 'THR', 'SEC', 'PYL',
+
+    'DAL', // D-ALANINE
+    'DAR', // D-ARGININE
+    'DSG', // D-ASPARAGINE
+    'DAS', // D-ASPARTIC ACID
+    'DCY', // D-CYSTEINE
+    'DGL', // D-GLUTAMIC ACID
+    'DGN', // D-GLUTAMINE
+    'DHI', // D-HISTIDINE
+    'DIL', // D-ISOLEUCINE
+    'DLE', // D-LEUCINE
+    'DLY', // D-LYSINE
+    'MED', // D-METHIONINE
+    'DPN', // D-PHENYLALANINE
+    'DPR', // D-PROLINE
+    'DSN', // D-SERINE
+    'DTH', // D-THREONINE
+    'DTR', // D-TRYPTOPHAN
+    'DTY', // D-TYROSINE
+    'DVA', // D-VALINE
+    'DNE' // D-NORLEUCINE
+    // ???  // D-SELENOCYSTEINE
+])
+
+export const RnaBaseNames = new Set([ 'A', 'C', 'T', 'G', 'I', 'U' ])
+export const DnaBaseNames = new Set([ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ])
+export const PeptideBaseNames = new Set([ 'APN', 'CPN', 'TPN', 'GPN' ])
+export const PurinBaseNames = new Set([ 'A', 'G', 'DA', 'DG', 'DI', 'APN', 'GPN' ])
+export const PyrimidineBaseNames = new Set([ 'C', 'T', 'U', 'DC', 'DT', 'DU', 'CPN', 'TPN' ])
+export const BaseNames = SetUtils.unionMany(RnaBaseNames, DnaBaseNames, PeptideBaseNames)
+
+export const isPurinBase = (compId: string) => PurinBaseNames.has(compId.toUpperCase())
+export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.has(compId.toUpperCase())
 
 /** get the molecule type from component type and id */
 export function getMoleculeType(compType: string, compId: string) {
     compType = compType.toUpperCase()
     compId = compId.toUpperCase()
-    if (PeptideBaseNames.includes(compId)) {
+    if (PeptideBaseNames.has(compId)) {
         return MoleculeType.PNA
-    } else if (ProteinComponentTypeNames.includes(compType)) {
+    } else if (ProteinComponentTypeNames.has(compType)) {
         return MoleculeType.protein
-    } else if (RNAComponentTypeNames.includes(compType)) {
+    } else if (RNAComponentTypeNames.has(compType)) {
         return MoleculeType.RNA
     } else if (DNAComponentTypeNames.includes(compType)) {
         return MoleculeType.DNA
-    } else if (SaccharideComponentTypeNames.includes(compType) || ExtraSaccharideNames.includes(compId)) {
+    } else if (SaccharideComponentTypeNames.has(compType)) {
         return MoleculeType.saccharide
-    } else if (WaterNames.includes(compId)) {
+    } else if (WaterNames.has(compId)) {
         return MoleculeType.water
-    } else if (IonNames.includes(compId)) {
+    } else if (IonNames.has(compId)) {
         return MoleculeType.ion
+    } else if (OtherComponentTypeNames.has(compType)) {
+        return MoleculeType.other
     } else {
         return MoleculeType.unknown
     }
 }
 
+export function getComponentType(compId: string): mmCIF_Schema['chem_comp']['type']['T'] {
+    compId = compId.toUpperCase()
+    if (AminoAcidNames.has(compId)) {
+        return 'peptide linking'
+    } else if (RnaBaseNames.has(compId)) {
+        return 'RNA linking'
+    } else if (DnaBaseNames.has(compId)) {
+        return 'DNA linking'
+    } else if (SaccharideCompIdMap.has(compId)) {
+        return 'saccharide'
+    } else {
+        return 'other'
+    }
+}
+
 export function isPolymer(moleculeType: MoleculeType) {
     return moleculeType === MoleculeType.protein || moleculeType === MoleculeType.DNA || moleculeType === MoleculeType.RNA || moleculeType === MoleculeType.PNA
 }
@@ -199,6 +228,8 @@ export function isNucleic(moleculeType: MoleculeType) {
 }
 
 /**
+ * TODO write script that read CCD and outputs list of ion names
+ *
  * all chemical components with the word "ion" in their name, Sep 2016
  *
  * SET SESSION group_concat_max_len = 1000000;
@@ -209,7 +240,7 @@ export function isNucleic(moleculeType: MoleculeType) {
  *     GROUP BY id_
  * ) AS t1;
  */
-export const IonNames = [
+export const IonNames = new Set([
   '118', '119', '1AL', '1CU', '2FK', '2HP', '2OF', '3CO',
   '3MT', '3NI', '3OF', '3P8', '4MO', '4PU', '543', '6MO', 'ACT', 'AG', 'AL',
   'ALF', 'AM', 'ATH', 'AU', 'AU3', 'AUC', 'AZI', 'BA', 'BCT', 'BEF', 'BF4', 'BO4',
@@ -230,14 +261,10 @@ export const IonNames = [
   'YB2', 'YH', 'YT3', 'ZCM', 'ZN', 'ZN2', 'ZN3', 'ZNO', 'ZO3',
     // additional ion names
   'OHX'
-]
+])
 
-export interface SecondaryStructureType extends BitFlags<SecondaryStructureType.Flag> { }
+export type SecondaryStructureType = BitFlags<SecondaryStructureType.Flag>
 export namespace SecondaryStructureType {
-    export const Helix = ['h', 'g', 'i']
-    export const Sheet = ['e', 'b']
-    export const Turn = ['s', 't', 'l', '']
-
     export const is: (ss: SecondaryStructureType, f: Flag) => boolean = BitFlags.has
     export const create: (fs: Flag) => SecondaryStructureType = BitFlags.create
 
@@ -348,7 +375,6 @@ export namespace SecondaryStructureType {
         I: Flag.Helix | Flag.HelixPi,  // PI-helix
         E: Flag.Beta | Flag.BetaSheet,  // Extended conformation
         B: Flag.Beta | Flag.BetaStrand,  // Isolated bridge
-        b: Flag.Beta | Flag.BetaStrand,  // Isolated bridge
         T: Flag.Turn,  // Turn
         C: Flag.NA,  // Coil (none of the above)
     }
@@ -485,7 +511,7 @@ export const VdwRadii = {
 }
 export const DefaultVdwRadius = 2.0
 
-export interface LinkType extends BitFlags<LinkType.Flag> { }
+export type LinkType = BitFlags<LinkType.Flag>
 export namespace LinkType {
     export const is: (b: LinkType, f: Flag) => boolean = BitFlags.has
     export const enum Flag {
diff --git a/src/mol-model/structure/query/queries/combinators.ts b/src/mol-model/structure/query/queries/combinators.ts
index 7bd531bd341446c61b474621f8f44c94d17d9446..a3b060ae65ad4453bc0d31777e27cc6420b6ccef 100644
--- a/src/mol-model/structure/query/queries/combinators.ts
+++ b/src/mol-model/structure/query/queries/combinators.ts
@@ -47,7 +47,7 @@ export function intersect(queries: ArrayLike<StructureQuery>): StructureQuery {
         }
 
         ctx.throwIfTimedOut();
-        const pivotSet = HashSet<Structure>(s => s.hashCode, Structure.areEqual);
+        const pivotSet = HashSet<Structure>(s => s.hashCode, Structure.areUnitAndIndicesEqual);
         StructureSelection.forEach(selections[pivotIndex], s => pivotSet.add(s));
 
         const ret = StructureSelection.UniqueBuilder(ctx.inputStructure);
diff --git a/src/mol-model/structure/query/queries/filters.ts b/src/mol-model/structure/query/queries/filters.ts
index 94aad107edce7c291256d06d021d5792cb5b16a6..a64cffc38bf15eae0a037b80153a2ac03b8d6fd4 100644
--- a/src/mol-model/structure/query/queries/filters.ts
+++ b/src/mol-model/structure/query/queries/filters.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { isSuperset } from 'mol-util/set';
+import { SetUtils } from 'mol-util/set';
 import { Unit } from '../../structure';
 import { QueryContext, QueryFn, QueryPredicate } from '../context';
 import { StructureQuery } from '../query';
@@ -82,7 +82,7 @@ export function withSameAtomProperties(query: StructureQuery, propertySource: St
         StructureSelection.forEach(sel, (s, i) => {
             ctx.currentStructure = s;
             const currentProps = getCurrentStructureProperties(ctx, props, new Set());
-            if (isSuperset(currentProps, propSet)) {
+            if (SetUtils.isSuperset(currentProps, propSet)) {
                 ret.add(s);
             }
 
diff --git a/src/mol-model/structure/query/queries/modifiers.ts b/src/mol-model/structure/query/queries/modifiers.ts
index 0b090451521bf67586a01120b292a467c1149683..f85855d26d81bf69d14fb66e3e67ba8cbd9b2431 100644
--- a/src/mol-model/structure/query/queries/modifiers.ts
+++ b/src/mol-model/structure/query/queries/modifiers.ts
@@ -279,11 +279,10 @@ export function expandProperty(query: StructureQuery, property: QueryFn): Struct
                 ctx.element.element = elements[i];
                 const p = property(ctx);
                 if (!propertyToStructureIndexMap.has(p)) continue;
-
                 const indices = propertyToStructureIndexMap.get(p)!.array;
 
                 for (let _sI = 0, __sI = indices.length; _sI < __sI; _sI++) {
-                    builders[indices[i]].addToUnit(unit.id, elements[i]);
+                    builders[indices[_sI]].addToUnit(unit.id, elements[i]);
                 }
             }
         }
diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts
index b257170712dc78158da19a0787c0f4718a8a615f..c13c370daaa5ea8783845a10d5303611cefd2c99 100644
--- a/src/mol-model/structure/query/selection.ts
+++ b/src/mol-model/structure/query/selection.ts
@@ -36,21 +36,47 @@ namespace StructureSelection {
     }
 
     export function toLoci(sel: StructureSelection): StructureElement.Loci {
-        const loci: { unit: Unit, indices: OrderedSet<StructureElement.UnitIndex> }[] = [];
+        const elements: { unit: Unit, indices: OrderedSet<StructureElement.UnitIndex> }[] = [];
         const { unitMap } = sel.source;
 
         for (const unit of unionStructure(sel).units) {
             if (unit === unitMap.get(unit.id)) {
-                loci[loci.length] = { unit, indices: OrderedSet.ofBounds(0 as StructureElement.UnitIndex, unit.elements.length as StructureElement.UnitIndex) };
+                elements[elements.length] = {
+                    unit,
+                    indices: OrderedSet.ofBounds(0 as StructureElement.UnitIndex, unit.elements.length as StructureElement.UnitIndex)
+                };
+            } else {
+                elements[elements.length] = {
+                    unit,
+                    indices: OrderedSet.ofSortedArray(SortedArray.indicesOf(unitMap.get(unit.id).elements, unit.elements))
+                };
+            }
+        }
+
+        return StructureElement.Loci(sel.source, elements);
+    }
+
+    /** use source unit in loci.elements */
+    export function toLoci2(sel: StructureSelection): StructureElement.Loci {
+        const elements: { unit: Unit, indices: OrderedSet<StructureElement.UnitIndex> }[] = [];
+        const { unitMap } = sel.source;
+
+        for (const _unit of unionStructure(sel).units) {
+            const unit = unitMap.get(_unit.id)
+            if (unit === _unit) {
+                elements[elements.length] = {
+                    unit,
+                    indices: OrderedSet.ofBounds(0 as StructureElement.UnitIndex, unit.elements.length as StructureElement.UnitIndex)
+                };
             } else {
-                loci[loci.length] = {
+                elements[elements.length] = {
                     unit,
-                    indices: OrderedSet.ofSortedArray(SortedArray.indicesOf(sel.source.unitMap.get(unit.id).elements, unit.elements))
+                    indices: OrderedSet.ofSortedArray(SortedArray.indicesOf(unit.elements, _unit.elements))
                 };
             }
         }
 
-        return StructureElement.Loci(sel.source, loci);
+        return StructureElement.Loci(sel.source, elements);
     }
 
     export interface Builder {
@@ -84,7 +110,7 @@ namespace StructureSelection {
     class HashBuilderImpl implements Builder {
         private structures: Structure[] = [];
         private allSingletons = true;
-        private uniqueSets = HashSet(Structure.hashCode, Structure.areEqual);
+        private uniqueSets = HashSet(Structure.hashCode, Structure.areUnitAndIndicesEqual);
 
         add(structure: Structure) {
             const atomCount = structure.elementCount;
diff --git a/src/mol-model/structure/query/utils/builders.ts b/src/mol-model/structure/query/utils/builders.ts
index 9da400cba184893cda559a225bd966b26357b5f1..a47a5354b539fc3ac160ee3b1345e2b344336bd0 100644
--- a/src/mol-model/structure/query/utils/builders.ts
+++ b/src/mol-model/structure/query/utils/builders.ts
@@ -12,7 +12,7 @@ import { StructureSubsetBuilder } from '../../structure/util/subset-builder';
 import { ElementIndex } from '../../model';
 
 export class UniqueStructuresBuilder {
-    private set = HashSet(Structure.hashCode, Structure.areEqual);
+    private set = HashSet(Structure.hashCode, Structure.areUnitAndIndicesEqual);
     private structures: Structure[] = [];
     private allSingletons = true;
 
diff --git a/src/mol-model/structure/query/utils/structure-set.ts b/src/mol-model/structure/query/utils/structure-set.ts
index 85867419505a8e3494e2f7a9e70838f140c0a00c..0ab6a9bbfafe296799320aa3f5d22e7f5d6b28b8 100644
--- a/src/mol-model/structure/query/utils/structure-set.ts
+++ b/src/mol-model/structure/query/utils/structure-set.ts
@@ -94,7 +94,7 @@ export function structureSubtract(a: Structure, b: Structure): Structure {
         const u = aU[i];
         if (!bU.has(u.id)) continue;
         const v = bU.get(u.id);
-        const sub = SortedArray.intersect(u.elements, v.elements);
+        const sub = SortedArray.subtract(u.elements, v.elements);
         if (sub.length > 0) {
             units[units.length] = u.getChild(sub);
         }
diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts
index 24594303dae766cec31f905f2952992ca6a4159b..3e0ec4ec82004ed7b7d30d7d4a56716a61a35acd 100644
--- a/src/mol-model/structure/structure/element.ts
+++ b/src/mol-model/structure/structure/element.ts
@@ -4,11 +4,19 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { OrderedSet, SortedArray } from 'mol-data/int'
-import Unit from './unit'
+import { UniqueArray } from 'mol-data/generic';
+import { OrderedSet, SortedArray } from 'mol-data/int';
+import { BoundaryHelper } from 'mol-math/geometry/boundary-helper';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
 import { ElementIndex } from '../model';
-import { ResidueIndex, ChainIndex } from '../model/indexing';
+import { ChainIndex, ResidueIndex } from '../model/indexing';
 import Structure from './structure';
+import Unit from './unit';
+import { Boundary } from './util/boundary';
+import { StructureProperties } from '../structure';
+import { sortArray } from 'mol-data/util';
+import Expression from 'mol-script/language/expression';
 
 interface StructureElement<U = Unit> {
     readonly kind: 'element-location',
@@ -35,11 +43,11 @@ namespace StructureElement {
 
     function _wrongUnitKind(kind: string) { throw new Error(`Property only available for ${kind} models.`); }
     export function atomicProperty<T>(p: (location: StructureElement<Unit.Atomic>) => T) {
-        return property(l => Unit.isAtomic(l.unit) ? p(l as StructureElement<Unit.Atomic>) : _wrongUnitKind('atomic') );
+        return property(l => Unit.isAtomic(l.unit) ? p(l as StructureElement<Unit.Atomic>) : _wrongUnitKind('atomic'));
     }
 
     export function coarseProperty<T>(p: (location: StructureElement<Unit.Spheres | Unit.Gaussians>) => T) {
-        return property(l => Unit.isCoarse(l.unit) ? p(l as StructureElement<Unit.Spheres | Unit.Gaussians>) : _wrongUnitKind('coarse') );
+        return property(l => Unit.isCoarse(l.unit) ? p(l as StructureElement<Unit.Spheres | Unit.Gaussians>) : _wrongUnitKind('coarse'));
     }
 
     /** Represents multiple element index locations */
@@ -108,6 +116,219 @@ namespace StructureElement {
                 return l.unit.model.coarseHierarchy.gaussians.entityKey[l.element]
         }
     }
+
+    export namespace Loci {
+        export function all(structure: Structure): Loci {
+            return Loci(structure, structure.units.map(unit => ({
+                unit,
+                indices: OrderedSet.ofRange<UnitIndex>(0 as UnitIndex, unit.elements.length as UnitIndex)
+            })));
+        }
+
+        export function remap(loci: Loci, structure: Structure): Loci {
+            return Loci(structure, loci.elements.map(e => ({
+                unit: structure.unitMap.get(e.unit.id)!,
+                indices: e.indices
+            })));
+        }
+
+        export function union(xs: Loci, ys: Loci): Loci {
+            if (xs.elements.length > ys.elements.length) return union(ys, xs);
+            if (xs.elements.length === 0) return ys;
+
+            const map = new Map<number, OrderedSet<UnitIndex>>();
+
+            for (const e of xs.elements) map.set(e.unit.id, e.indices);
+
+            const elements: Loci['elements'][0][] = [];
+            for (const e of ys.elements) {
+                if (map.has(e.unit.id)) {
+                    elements[elements.length] = { unit: e.unit, indices: OrderedSet.union(map.get(e.unit.id)!, e.indices) };
+                } else {
+                    elements[elements.length] = e;
+                }
+            }
+
+            return Loci(xs.structure, elements);
+        }
+
+        export function subtract(xs: Loci, ys: Loci): Loci {
+            const map = new Map<number, OrderedSet<UnitIndex>>();
+            for (const e of ys.elements) map.set(e.unit.id, e.indices);
+
+            const elements: Loci['elements'][0][] = [];
+            for (const e of xs.elements) {
+                if (map.has(e.unit.id)) {
+                    const indices = OrderedSet.subtract(e.indices, map.get(e.unit.id)!);
+                    if (OrderedSet.size(indices) === 0) continue;
+                    elements[elements.length] = { unit: e.unit, indices };
+                } else {
+                    elements[elements.length] = e;
+                }
+            }
+
+            return Loci(xs.structure, elements);
+        }
+
+        export function areIntersecting(xs: Loci, ys: Loci): boolean {
+            if (xs.elements.length > ys.elements.length) return areIntersecting(ys, xs);
+            if (xs.elements.length === 0) return ys.elements.length === 0;
+
+            const map = new Map<number, OrderedSet<UnitIndex>>();
+
+            for (const e of xs.elements) map.set(e.unit.id, e.indices);
+            for (const e of ys.elements) {
+                if (!map.has(e.unit.id)) continue;
+                if (OrderedSet.areIntersecting(map.get(e.unit.id)!, e.indices)) return true;
+            }
+
+            return false;
+        }
+
+        export function extendToWholeResidues(loci: Loci): Loci {
+            const elements: Loci['elements'][0][] = [];
+
+            for (const lociElement of loci.elements) {
+                if (lociElement.unit.kind !== Unit.Kind.Atomic) elements[elements.length] = lociElement;
+
+                const unitElements = lociElement.unit.elements;
+                const h = lociElement.unit.model.atomicHierarchy;
+
+                const { index: residueIndex, offsets: residueOffsets } = h.residueAtomSegments;
+
+                const newIndices: UnitIndex[] = [];
+                const indices = lociElement.indices, len = OrderedSet.size(indices);
+                let i = 0;
+                while (i < len) {
+                    const rI = residueIndex[unitElements[OrderedSet.getAt(indices, i)]];
+                    i++;
+                    while (i < len && residueIndex[unitElements[OrderedSet.getAt(indices, i)]] === rI) {
+                        i++;
+                    }
+
+                    for (let j = residueOffsets[rI], _j = residueOffsets[rI + 1]; j < _j; j++) {
+                        const idx = OrderedSet.indexOf(unitElements, j);
+                        if (idx >= 0) newIndices[newIndices.length] = idx as UnitIndex;
+                    }
+                }
+
+                elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
+            }
+
+            return Loci(loci.structure, elements);
+        }
+
+        const boundaryHelper = new BoundaryHelper(), tempPos = Vec3.zero();
+        export function getBoundary(loci: Loci): Boundary {
+            boundaryHelper.reset(0);
+
+            for (const e of loci.elements) {
+                const { indices } = e;
+                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+                const { elements } = e.unit;
+                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                    const eI = elements[OrderedSet.getAt(indices, i)];
+                    pos(eI, tempPos);
+                    boundaryHelper.boundaryStep(tempPos, r(eI));
+                }
+            }
+            boundaryHelper.finishBoundaryStep();
+            for (const e of loci.elements) {
+                const { indices } = e;
+                const pos = e.unit.conformation.position, r = e.unit.conformation.r;
+                const { elements } = e.unit;
+                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                    const eI = elements[OrderedSet.getAt(indices, i)];
+                    pos(eI, tempPos);
+                    boundaryHelper.extendStep(tempPos, r(eI));
+                }
+            }
+
+            return { box: boundaryHelper.getBox(), sphere: boundaryHelper.getSphere() };
+        }
+
+        export function toScriptExpression(loci: Loci) {
+            if (loci.structure.models.length > 1) {
+                console.warn('toScriptExpression is only supported for Structure with single model, returning empty expression.');
+                return MS.struct.generator.empty();
+            }
+            if (loci.elements.length === 0) return MS.struct.generator.empty();
+
+            const sourceIndexMap = new Map<string, UniqueArray<number, number>>();
+            const el = StructureElement.create(), p = StructureProperties.atom.sourceIndex;
+            for (const e of loci.elements) {
+                const { indices } = e;
+                const { elements } = e.unit;
+                const opName = e.unit.conformation.operator.name;
+
+                let sourceIndices: UniqueArray<number, number>;
+                if (sourceIndexMap.has(opName)) sourceIndices = sourceIndexMap.get(opName)!;
+                else {
+                    sourceIndices = UniqueArray.create<number, number>();
+                    sourceIndexMap.set(opName, sourceIndices);
+                }
+
+                el.unit = e.unit;
+                for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) {
+                    el.element = elements[OrderedSet.getAt(indices, i)];
+                    const idx = p(el);
+                    UniqueArray.add(sourceIndices, idx, idx);
+                }
+            }
+
+            const byOpName: Expression[] = [];
+            const keys = sourceIndexMap.keys();
+            while (true) {
+                const k = keys.next();
+                if (k.done) break;
+                byOpName.push(getOpNameQuery(k.value, sourceIndexMap.get(k.value)!.array));
+            }
+
+            return MS.struct.modifier.union([
+                byOpName.length === 1 ? byOpName[0] : MS.struct.combinator.merge.apply(null, byOpName)
+            ]);
+        }
+
+        function getOpNameQuery(opName: string, xs: number[]) {
+            sortArray(xs);
+
+            const ranges: number[] = [];
+            const set: number[] = [];
+
+            let i = 0, len = xs.length;
+            while (i < len) {
+                const start = i;
+                i++;
+                while (i < len && xs[i - 1] + 1 === xs[i]) i++;
+                const end = i;
+                // TODO: is this a good value?
+                if (end - start > 12) {
+                    ranges[ranges.length] = xs[start];
+                    ranges[ranges.length] = xs[end - 1];
+                } else {
+                    for (let j = start; j < end; j++) {
+                        set[set.length] = xs[j];
+                    }
+                }
+            }
+
+            const siProp = MS.struct.atomProperty.core.sourceIndex();
+            const tests: Expression[] = [];
+
+            // TODO: add set.ofRanges constructor to MolQL???
+            if (set.length > 0) {
+                tests[tests.length] = MS.core.set.has([MS.set.apply(null, set), siProp]);
+            }
+            for (let rI = 0, _rI = ranges.length / 2; rI < _rI; rI++) {
+                tests[tests.length] = MS.core.rel.inRange([siProp, ranges[2 * rI], ranges[2 * rI + 1]]);
+            }
+
+            return MS.struct.generator.atomGroups({
+                'atom-test': tests.length > 1 ? MS.core.logic.or.apply(null, tests) : tests[0],
+                'chain-test': MS.core.rel.eq([MS.struct.atomProperty.core.operatorName(), opName])
+            });
+        }
+    }
 }
 
 export default StructureElement
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/properties.ts b/src/mol-model/structure/structure/properties.ts
index b2cfa785cdc9534bf57ea26dc13bb252b7db3904..ce4d02dc13be2d0e5768a008534d8bfd034783cb 100644
--- a/src/mol-model/structure/structure/properties.ts
+++ b/src/mol-model/structure/structure/properties.ts
@@ -35,6 +35,10 @@ const atom = {
     id: StructureElement.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicConformation.atomId.value(l.element)),
     occupancy: StructureElement.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.occupancy.value(l.element)),
     B_iso_or_equiv: StructureElement.property(l => !Unit.isAtomic(l.unit) ?  notAtomic() : l.unit.model.atomicConformation.B_iso_or_equiv.value(l.element)),
+    sourceIndex: StructureElement.property(l => Unit.isAtomic(l.unit)
+        ? l.unit.model.atomicHierarchy.atoms.sourceIndex.value(l.element)
+        // TODO: when implemented, this should map to the source index.
+        : l.element),
 
     // Hierarchy
     type_symbol: StructureElement.property(l => !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.atoms.type_symbol.value(l.element)),
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index d7b99e9ff018db30162d37aae9a615c2892c2e20..8d45b51467ac1f67d2d734e7fd12c70efddbd444 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -72,6 +72,10 @@ class Structure {
         return prc && ec ? ec / prc < 2 : false
     }
 
+    get isEmpty() {
+        return this.units.length === 0;
+    }
+
     get hashCode() {
         if (this._props.hashCode !== -1) return this._props.hashCode;
         return this.computeHash();
@@ -377,7 +381,7 @@ namespace Structure {
         const units: Unit[] = [];
         for (const u of s.units) {
             const old = u.conformation.operator;
-            const op = SymmetryOperator.create(old.name, transform, { id: '', operList: [] }, old.hkl);
+            const op = SymmetryOperator.create(old.name, transform, { id: '', operList: [] }, old.ncsId, old.hkl);
             units.push(u.applyOperator(u.id, op));
         }
 
@@ -420,7 +424,7 @@ namespace Structure {
         return hashString(s.units.map(u => Unit.conformationId(u)).join('|'))
     }
 
-    export function areEqual(a: Structure, b: Structure) {
+    export function areUnitAndIndicesEqual(a: Structure, b: Structure) {
         if (a.elementCount !== b.elementCount) return false;
         const len = a.units.length;
         if (len !== b.units.length) return false;
diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts
index 5af14696a9e1b5017299ace7cbe5643956a2bc1f..1b917041444704a539124a1d2800354e8fdebb6e 100644
--- a/src/mol-model/structure/structure/symmetry.ts
+++ b/src/mol-model/structure/structure/symmetry.ts
@@ -1,19 +1,19 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Structure from './structure'
-import { StructureSelection, QueryContext } from '../query'
-import { ModelSymmetry } from '../model'
-import { Task, RuntimeContext } from 'mol-task';
 import { SortedArray } from 'mol-data/int';
-import Unit from './unit';
 import { EquivalenceClasses } from 'mol-data/util';
-import { Vec3 } from 'mol-math/linear-algebra';
-import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry';
+import { Spacegroup, SpacegroupCell, SymmetryOperator } from 'mol-math/geometry';
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { RuntimeContext, Task } from 'mol-task';
+import { ModelSymmetry } from '../model';
+import { QueryContext, StructureSelection } from '../query';
+import Structure from './structure';
+import Unit from './unit';
 
 namespace StructureSymmetry {
     export function buildAssembly(structure: Structure, asmName: string) {
@@ -90,26 +90,47 @@ namespace StructureSymmetry {
 }
 
 function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {
-    const operators: SymmetryOperator[] = symmetry._operators_333 || [];
+    const operators: SymmetryOperator[] = [];
     const { spacegroup } = symmetry;
-    if (operators.length === 0) {
+    if (ijkMin[0] <= 0 && ijkMax[0] >= 0 &&
+        ijkMin[1] <= 0 && ijkMax[1] >= 0 &&
+        ijkMin[2] <= 0 && ijkMax[2] >= 0) {
         operators[0] = Spacegroup.getSymmetryOperator(spacegroup, 0, 0, 0, 0)
-        for (let op = 0; op < spacegroup.operators.length; op++) {
-            for (let i = ijkMin[0]; i < ijkMax[0]; i++) {
-                for (let j = ijkMin[1]; j < ijkMax[1]; j++) {
-                    for (let k = ijkMin[2]; k < ijkMax[2]; k++) {
-                        // we have added identity as the 1st operator.
-                        if (op === 0 && i === 0 && j === 0 && k === 0) continue;
-                        operators[operators.length] = Spacegroup.getSymmetryOperator(spacegroup, op, i, j, k);
+    }
+
+    const { ncsOperators } = symmetry
+    const ncsCount = (ncsOperators && ncsOperators.length) || 0
+
+    for (let op = 0; op < spacegroup.operators.length; op++) {
+        for (let i = ijkMin[0]; i <= ijkMax[0]; i++) {
+            for (let j = ijkMin[1]; j <= ijkMax[1]; j++) {
+                for (let k = ijkMin[2]; k <= ijkMax[2]; k++) {
+                    // we have added identity as the 1st operator.
+                    if (op === 0 && i === 0 && j === 0 && k === 0) continue;
+                    const symOp = Spacegroup.getSymmetryOperator(spacegroup, op, i, j, k);
+                    if (ncsCount) {
+                        for (let u = 0; u < ncsCount; ++u) {
+                            const ncsOp = ncsOperators![u]
+                            const matrix = Mat4.mul(Mat4.zero(), symOp.matrix, ncsOp.matrix)
+                            const operator = SymmetryOperator.create(`${symOp.name} ${ncsOp.name}`, matrix, symOp.assembly, ncsOp.ncsId, symOp.hkl);
+                            operators[operators.length] = operator;
+                        }
+                    } else {
+                        operators[operators.length] = symOp;
                     }
                 }
             }
         }
-        symmetry._operators_333 = operators;
     }
     return operators;
 }
 
+function getOperatorsCached333(symmetry: ModelSymmetry) {
+    if (typeof symmetry._operators_333 !== 'undefined') return symmetry._operators_333;
+    symmetry._operators_333 = getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3));
+    return symmetry._operators_333;
+}
+
 function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
     const assembler = Structure.Builder();
     const { units } = structure;
@@ -150,7 +171,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
     if (SpacegroupCell.isZero(spacegroup.cell)) return structure;
 
     if (ctx.shouldUpdate) await ctx.update('Initialing...');
-    const operators = getOperators(symmetry, Vec3.create(-3, -3, -3), Vec3.create(3, 3, 3));
+    const operators = getOperatorsCached333(symmetry);
     const lookup = structure.lookup3d;
 
     const assembler = Structure.Builder();
diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts
index cbdf58718698e9342657f0739e5fc4b832ef27ee..258141272e6fe52857ea23f20a2619b94e37fdf9 100644
--- a/src/mol-model/structure/structure/unit.ts
+++ b/src/mol-model/structure/structure/unit.ts
@@ -7,7 +7,7 @@
 
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model } from '../model'
-import { GridLookup3D, Lookup3D, DensityData } from 'mol-math/geometry'
+import { GridLookup3D, Lookup3D } from 'mol-math/geometry'
 import { IntraUnitLinks, computeIntraUnitBonds } from './unit/links'
 import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation } from '../model/properties/coarse';
 import { ValueRef } from 'mol-util';
@@ -18,9 +18,6 @@ import { IntMap, SortedArray } from 'mol-data/int';
 import { hash2, hashFnv32a } from 'mol-data/util';
 import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer';
 import { getNucleotideElements } from './util/nucleotide';
-import { GaussianDensityProps, computeUnitGaussianDensityCached } from './unit/gaussian-density';
-import { RuntimeContext } from 'mol-task';
-import { WebGLContext } from 'mol-gl/webgl/context';
 
 /**
  * A building block of a structure that corresponds to an atomic or
@@ -82,6 +79,13 @@ namespace Unit {
         }
     }
 
+    export namespace SymmetryGroup {
+        export function areInvariantElementsEqual(a: SymmetryGroup, b: SymmetryGroup) {
+            if (a.hashCode !== b.hashCode) return false;
+            return SortedArray.areEqual(a.elements, b.elements);
+        }
+    }
+
     export function conformationId (unit: Unit) {
         return Unit.isAtomic(unit) ? unit.model.atomicConformation.id : unit.model.coarseConformation.id
     }
@@ -191,10 +195,6 @@ namespace Unit {
             return this.model.atomicHierarchy.residueAtomSegments.index[this.elements[elementIndex]];
         }
 
-        async computeGaussianDensity(props: GaussianDensityProps, ctx: RuntimeContext, webgl?: WebGLContext) {
-            return computeUnitGaussianDensityCached(this, props, this.props.gaussianDensities, ctx, webgl);
-        }
-
         constructor(id: number, invariantId: number, model: Model, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: AtomicProperties) {
             this.id = id;
             this.invariantId = invariantId;
@@ -215,7 +215,6 @@ namespace Unit {
         polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
         gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
         nucleotideElements: ValueRef<SortedArray<ElementIndex> | undefined>
-        gaussianDensities: Map<string, DensityData>
     }
 
     function AtomicProperties(): AtomicProperties {
@@ -226,7 +225,6 @@ namespace Unit {
             polymerElements: ValueRef.create(void 0),
             gapElements: ValueRef.create(void 0),
             nucleotideElements: ValueRef.create(void 0),
-            gaussianDensities: new Map()
         };
     }
 
@@ -280,10 +278,6 @@ namespace Unit {
             return this.kind === Kind.Spheres ? this.model.coarseConformation.spheres : this.model.coarseConformation.gaussians;
         }
 
-        async computeGaussianDensity(props: GaussianDensityProps, ctx: RuntimeContext, webgl?: WebGLContext): Promise<DensityData> {
-            return computeUnitGaussianDensityCached(this as Unit.Spheres | Unit.Gaussians, props, this.props.gaussianDensities, ctx, webgl); // TODO get rid of casting
-        }
-
         constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties) {
             this.kind = kind;
             this.id = id;
@@ -299,7 +293,6 @@ namespace Unit {
 
     interface CoarseProperties {
         lookup3d: ValueRef<Lookup3D | undefined>,
-        gaussianDensities: Map<string, DensityData>
         polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
         gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
     }
@@ -307,7 +300,6 @@ namespace Unit {
     function CoarseProperties(): CoarseProperties {
         return {
             lookup3d: ValueRef.create(void 0),
-            gaussianDensities: new Map(),
             polymerElements: ValueRef.create(void 0),
             gapElements: ValueRef.create(void 0),
         };
diff --git a/src/mol-model/structure/structure/unit/gaussian-density.ts b/src/mol-model/structure/structure/unit/gaussian-density.ts
deleted file mode 100644
index cefbfcd68c8d90d2f39a2bef20af4ee51f148862..0000000000000000000000000000000000000000
--- a/src/mol-model/structure/structure/unit/gaussian-density.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Unit, StructureElement, ElementIndex } from 'mol-model/structure';
-import { GaussianDensity } from 'mol-math/geometry/gaussian-density';
-import { Task, RuntimeContext } from 'mol-task';
-import { DensityData } from 'mol-math/geometry';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { GaussianDensityTexture } from 'mol-math/geometry/gaussian-density/gpu';
-import { Texture } from 'mol-gl/webgl/texture';
-import { WebGLContext } from 'mol-gl/webgl/context';
-import { PhysicalSizeTheme } from 'mol-theme/size/physical';
-
-export const GaussianDensityParams = {
-    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
-    radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
-    smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
-    useGpu: PD.Boolean(false),
-    ignoreCache: PD.Boolean(false),
-}
-export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams)
-export type GaussianDensityProps = typeof DefaultGaussianDensityProps
-
-function getConformation(unit: Unit) {
-    switch (unit.kind) {
-        case Unit.Kind.Atomic: return unit.model.atomicConformation
-        case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres
-        case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians
-    }
-}
-
-function getConformationAndRadius(unit: Unit) {
-    const conformation = getConformation(unit)
-    const { elements } = unit
-    const position = {
-        indices: elements,
-        x: conformation.x,
-        y: conformation.y,
-        z: conformation.z
-    }
-
-    const l = StructureElement.create(unit)
-    const sizeTheme = PhysicalSizeTheme({}, {})
-    const radius = (index: number) => {
-        l.element = index as ElementIndex
-        return sizeTheme.size(l)
-    }
-
-    return { position, radius }
-}
-
-export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) {
-    const { position, radius } = getConformationAndRadius(unit)
-    return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensity(ctx, position, unit.lookup3d.boundary.box, radius, props, webgl);
-    });
-}
-
-export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
-    const { position, radius } = getConformationAndRadius(unit)
-    return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensityTexture(ctx, webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
-    });
-}
-
-export async function computeUnitGaussianDensityCached(unit: Unit, props: GaussianDensityProps, cache: Map<string, DensityData>, ctx: RuntimeContext, webgl?: WebGLContext) {
-    const key = `${props.radiusOffset}|${props.resolution}|${props.smoothness}`
-    let density = cache.get(key)
-    if (density && !props.ignoreCache) return density
-    density = await computeUnitGaussianDensity(unit, props, webgl).runInContext(ctx)
-    if (!props.ignoreCache) cache.set(key, density)
-    return density
-}
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/unit/links/data.ts b/src/mol-model/structure/structure/unit/links/data.ts
index 00559aec5a5cb63269a7ef784ccc5b7bb8d145d0..d8f40a7ec87e3328bb1adc1d763928b08c79bba2 100644
--- a/src/mol-model/structure/structure/unit/links/data.ts
+++ b/src/mol-model/structure/structure/unit/links/data.ts
@@ -66,10 +66,10 @@ class InterUnitBonds {
                 pairBonds.linkedElementIndices.forEach(indexA => {
                     pairBonds.getBonds(indexA).forEach(bondInfo => {
                         const { unitA, unitB } = pairBonds
-                        
+
                         const bondKey = InterUnitBonds.getBondKey(indexA, unitA, bondInfo.indexB, unitB)
                         bondKeyIndex.set(bondKey, bonds.length)
-                        
+
                         const elementKey = InterUnitBonds.getElementKey(indexA, unitA)
                         const e = elementKeyIndex.get(elementKey)
                         if (e === undefined) elementKeyIndex.set(elementKey, [bonds.length])
diff --git a/src/mol-model/structure/structure/unit/links/inter-compute.ts b/src/mol-model/structure/structure/unit/links/inter-compute.ts
index f4f0143449d11a83c936185027e17e9b068ad0f5..ac094daa8c3f143658e3783378f50e1ab2be1487 100644
--- a/src/mol-model/structure/structure/unit/links/inter-compute.ts
+++ b/src/mol-model/structure/structure/unit/links/inter-compute.ts
@@ -4,7 +4,6 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StructConn } from '../../../model/formats/mmcif/bonds';
 import { LinkType } from '../../../model/types';
 import Structure from '../../structure';
 import Unit from '../../unit';
@@ -14,6 +13,7 @@ import { UniqueArray } from 'mol-data/generic';
 import { SortedArray } from 'mol-data/int';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import StructureElement from '../../element';
+import { StructConn } from 'mol-model-formats/structure/mmcif/bonds';
 
 const MAX_RADIUS = 4;
 
diff --git a/src/mol-model/structure/structure/unit/links/intra-compute.ts b/src/mol-model/structure/structure/unit/links/intra-compute.ts
index 5ba8eb95b351a01f472f10132a16f190af4ece99..04b7497de8988bcda8b034a76fff8971c41e6834 100644
--- a/src/mol-model/structure/structure/unit/links/intra-compute.ts
+++ b/src/mol-model/structure/structure/unit/links/intra-compute.ts
@@ -6,11 +6,11 @@
 
 import { LinkType } from '../../../model/types'
 import { IntraUnitLinks } from './data'
-import { StructConn, ComponentBond } from '../../../model/formats/mmcif/bonds'
 import Unit from '../../unit'
 import { IntAdjacencyGraph } from 'mol-math/graph';
 import { LinkComputationParameters, getElementIdx, MetalsSet, getElementThreshold, isHydrogen, getElementPairThreshold } from './common';
 import { SortedArray } from 'mol-data/int';
+import { StructConn, ComponentBond } from 'mol-model-formats/structure/mmcif/bonds';
 
 function getGraph(atomA: number[], atomB: number[], _order: number[], _flags: number[], atomCount: number): IntraUnitLinks {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
diff --git a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
index 28ae9d1be1bb2d38ac20cd7a50206415f6acfd88..a6e027e9cc5937eecdcd7e39b129c186cbbb6871 100644
--- a/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
+++ b/src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
@@ -6,9 +6,9 @@
 
 import Unit from '../../unit';
 import Structure from '../../structure';
-import { IHMCrossLinkRestraint } from '../../../model/formats/mmcif/pair-restraint';
 import { PairRestraints, CrossLinkRestraint } from './data';
 import { StructureElement } from '../../../structure';
+import { IHMCrossLinkRestraint } from 'mol-model-formats/structure/mmcif/pair-restraint';
 
 function _addRestraints(map: Map<number, number>, unit: Unit, restraints: IHMCrossLinkRestraint) {
     const { elements } = unit;
diff --git a/src/mol-model/structure/structure/util/unit-transforms.ts b/src/mol-model/structure/structure/util/unit-transforms.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2569f17e43efdc3d5c9170cdc3b6208f045789f2
--- /dev/null
+++ b/src/mol-model/structure/structure/util/unit-transforms.ts
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure, Unit } from 'mol-model/structure';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { IntMap } from 'mol-data/int';
+import { fillIdentityTransform } from 'mol-geo/geometry/transform-data';
+
+export class StructureUnitTransforms {
+    private unitTransforms: Float32Array
+    private groupUnitTransforms: Float32Array[] = []
+    /** maps unit.id to offset of transform in unitTransforms */
+    private unitOffsetMap = IntMap.Mutable<number>();
+    private groupIndexMap = IntMap.Mutable<number>();
+    private size: number;
+
+    constructor(readonly structure: Structure) {
+        this.unitTransforms = new Float32Array(structure.units.length * 16)
+        this.size = structure.units.length
+        fillIdentityTransform(this.unitTransforms, structure.units.length)
+        let groupOffset = 0
+        for (let i = 0, il = structure.unitSymmetryGroups.length; i <il; ++i) {
+            const g = structure.unitSymmetryGroups[i]
+            this.groupIndexMap.set(g.hashCode, i)
+            const groupTransforms = this.unitTransforms.subarray(groupOffset, groupOffset + g.units.length * 16)
+            this.groupUnitTransforms.push(groupTransforms)
+            for (let j = 0, jl = g.units.length; j < jl; ++j) {
+                this.unitOffsetMap.set(g.units[j].id, groupOffset + j * 16)
+            }
+            groupOffset += g.units.length * 16
+        }
+    }
+
+    reset() {
+        fillIdentityTransform(this.unitTransforms, this.size);
+    }
+
+    setTransform(matrix: Mat4, unit: Unit) {
+        Mat4.toArray(matrix, this.unitTransforms, this.unitOffsetMap.get(unit.id))
+    }
+
+    getTransform(out: Mat4, unit: Unit) {
+        return Mat4.fromArray(out, this.unitTransforms, this.unitOffsetMap.get(unit.id))
+    }
+
+    getSymmetryGroupTransforms(group: Unit.SymmetryGroup): Float32Array {
+        return this.groupUnitTransforms[this.groupIndexMap.get(group.hashCode)]
+    }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/util.ts b/src/mol-model/structure/util.ts
index e688b99c2c88325f541e5184020b98dd5648bcd8..87e34b6e024c77874205055fa2b9448981dce467 100644
--- a/src/mol-model/structure/util.ts
+++ b/src/mol-model/structure/util.ts
@@ -1,11 +1,11 @@
 /**
- * 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 { Model, ResidueIndex, ElementIndex } from './model';
-import { MoleculeType, AtomRole, MoleculeTypeAtomRoleId } from './model/types';
+import { MoleculeType, AtomRole, MoleculeTypeAtomRoleId, getMoleculeType } from './model/types';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Unit } from './structure';
 import Matrix from 'mol-math/linear-algebra/matrix/matrix';
@@ -17,36 +17,32 @@ export function getCoarseBegCompId(unit: Unit.Spheres | Unit.Gaussians, element:
     return seq.compId.value(seq_id_begin - 1) // 1-indexed
 }
 
-export function getElementMoleculeType(unit: Unit, element: ElementIndex) {
-    let compId = ''
+export function getElementMoleculeType(unit: Unit, element: ElementIndex): MoleculeType {
     switch (unit.kind) {
         case Unit.Kind.Atomic:
-            compId = unit.model.atomicHierarchy.residues.label_comp_id.value(unit.residueIndex[element])
-            break
+            return unit.model.atomicHierarchy.derived.residue.moleculeType[unit.residueIndex[element]]
         case Unit.Kind.Spheres:
         case Unit.Kind.Gaussians:
-            compId = getCoarseBegCompId(unit, element)
-            break
+            // TODO add unit.model.coarseHierarchy.derived.residue.moleculeType
+            const compId = getCoarseBegCompId(unit, element)
+            const cc = unit.model.properties.chemicalComponentMap.get(compId)
+            if (cc) return getMoleculeType(cc.type, compId)
     }
-    const chemCompMap = unit.model.properties.chemicalComponentMap
-    const cc = chemCompMap.get(compId)
-    return cc ? cc.moleculeType : MoleculeType.unknown
+    return MoleculeType.unknown
 }
 
-export function getAtomicMoleculeType(model: Model, rI: ResidueIndex) {
-    const compId = model.atomicHierarchy.residues.label_comp_id.value(rI)
-    const chemCompMap = model.properties.chemicalComponentMap
-    const cc = chemCompMap.get(compId)
-    return cc ? cc.moleculeType : MoleculeType.unknown
+export function getAtomicMoleculeType(model: Model, rI: ResidueIndex): MoleculeType {
+    return model.atomicHierarchy.derived.residue.moleculeType[rI]
 }
 
+const EmptyAtomIds = new Set<string>()
 export function getAtomIdForAtomRole(moleculeType: MoleculeType, atomRole: AtomRole) {
     const m = MoleculeTypeAtomRoleId[moleculeType]
     if (m !== undefined) {
         const a = m[atomRole]
         if (a !== undefined) return a
     }
-    return ''
+    return EmptyAtomIds
 }
 
 export function residueLabel(model: Model, rI: number) {
@@ -57,6 +53,18 @@ export function residueLabel(model: Model, rI: number) {
     return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}`
 }
 
+export function elementLabel(model: Model, index: ElementIndex) {
+    const { atoms, residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy
+    const { label_atom_id } = atoms
+    const { auth_seq_id, auth_comp_id } = residues
+    const { auth_asym_id } = chains
+
+    const residueIndex = residueAtomSegments.index[index]
+    const chainIndex = chainAtomSegments.index[residueIndex]
+
+    return `[${auth_comp_id.value(residueIndex)}]${auth_seq_id.value(residueIndex)}:${auth_asym_id.value(chainIndex)}.${label_atom_id.value(index)}`
+}
+
 // const centerPos = Vec3.zero()
 // const centerMin = Vec3.zero()
 // export function getCenterAndRadius(centroid: Vec3, unit: Unit, indices: ArrayLike<number>) {
diff --git a/src/mol-model/volume.ts b/src/mol-model/volume.ts
index aa8cf1ce46056532d4723f8c3584de3842c4e484..c622e171bf076b1d77cca1d2f027464ac44734ee 100644
--- a/src/mol-model/volume.ts
+++ b/src/mol-model/volume.ts
@@ -4,5 +4,4 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-export * from './volume/data'
-export * from './volume/formats/density-server'
\ No newline at end of file
+export * from './volume/data'
\ No newline at end of file
diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts
index cda1035294766b00ea4edc8970c9bee925f1b95f..4f414675ee8b6c5bd6ba57dcfc59a674a3aa95bc 100644
--- a/src/mol-model/volume/data.ts
+++ b/src/mol-model/volume/data.ts
@@ -1,11 +1,13 @@
 /**
- * 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>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { SpacegroupCell, Box3D } from 'mol-math/geometry'
 import { Tensor, Mat4, Vec3 } from 'mol-math/linear-algebra'
+import { equalEps } from 'mol-math/linear-algebra/3d/common';
 
 /** The basic unit cell that contains the data. */
 interface VolumeData {
@@ -21,6 +23,13 @@ interface VolumeData {
 }
 
 namespace VolumeData {
+    export const One: VolumeData = {
+        cell: SpacegroupCell.Zero,
+        fractionalBox: Box3D.empty(),
+        data: Tensor.create(Tensor.Space([1, 1, 1], [0, 1, 2]), Tensor.Data1([0])),
+        dataStats: { min: 0, max: 0, mean: 0, sigma: 0 }
+    }
+
     const _scale = Mat4.zero(), _translate = Mat4.zero();
     export function getGridToCartesianTransform(volume: VolumeData) {
         const { data: { space } } = volume;
@@ -28,41 +37,45 @@ namespace VolumeData {
         const translate = Mat4.fromTranslation(_translate, volume.fractionalBox.min);
         return Mat4.mul3(Mat4.zero(), volume.cell.fromFractional, translate, scale);
     }
+
+    export function areEquivalent(volA: VolumeData, volB: VolumeData) {
+        return volA === volB
+    }
 }
 
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative
 
 namespace VolumeIsoValue {
-    export type Relative = Readonly<{ kind: 'relative', stats: VolumeData['dataStats'], relativeValue: number }>
-    export type Absolute = Readonly<{ kind: 'absolute', stats: VolumeData['dataStats'], absoluteValue: number }>
+    export type Relative = Readonly<{ kind: 'relative', relativeValue: number }>
+    export type Absolute = Readonly<{ kind: 'absolute', absoluteValue: number }>
 
-    export function absolute(stats: VolumeData['dataStats'], value: number): Absolute { return { kind: 'absolute', stats, absoluteValue: value }; }
-    export function relative(stats: VolumeData['dataStats'], value: number): Relative { return { kind: 'relative', stats, relativeValue: value }; }
+    export function areSame(a: VolumeIsoValue, b: VolumeIsoValue, stats: VolumeData['dataStats']) {
+        return equalEps(toAbsolute(a, stats).absoluteValue, toAbsolute(b, stats).absoluteValue, stats.sigma / 100)
+    }
+
+    export function absolute(value: number): Absolute { return { kind: 'absolute', absoluteValue: value }; }
+    export function relative(value: number): Relative { return { kind: 'relative', relativeValue: value }; }
 
     export function calcAbsolute(stats: VolumeData['dataStats'], relativeValue: number): number {
         return relativeValue * stats.sigma + stats.mean
     }
 
     export function calcRelative(stats: VolumeData['dataStats'], absoluteValue: number): number {
-        return (stats.mean - absoluteValue) / stats.sigma
+        return stats.sigma === 0 ? 0 : ((absoluteValue - stats.mean) / stats.sigma)
+    }
+
+    export function toAbsolute(value: VolumeIsoValue, stats: VolumeData['dataStats']): Absolute {
+        return value.kind === 'absolute' ? value : { kind: 'absolute', absoluteValue: VolumeIsoValue.calcAbsolute(stats, value.relativeValue) }
     }
 
-    export function toAbsolute(value: VolumeIsoValue): Absolute {
-        if (value.kind === 'absolute') return value;
-        return {
-            kind: 'absolute',
-            stats: value.stats,
-            absoluteValue: calcAbsolute(value.stats, value.relativeValue)
-        }
+    export function toRelative(value: VolumeIsoValue, stats: VolumeData['dataStats']): Relative {
+        return value.kind === 'relative' ? value : { kind: 'relative', relativeValue: VolumeIsoValue.calcRelative(stats, value.absoluteValue) }
     }
 
-    export function toRelative(value: VolumeIsoValue): Relative {
-        if (value.kind === 'relative') return value;
-        return {
-            kind: 'relative',
-            stats: value.stats,
-            relativeValue: calcRelative(value.stats, value.absoluteValue)
-        }
+    export function toString(value: VolumeIsoValue) {
+        return value.kind === 'relative'
+            ? `${value.relativeValue} σ`
+            : `${value.absoluteValue}`
     }
 }
 
diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts
index 86339477be343557bbb6f51a1d3229cd14462db7..a079d9eff4213f48b67838c4f2c521805b12f4a3 100644
--- a/src/mol-plugin/behavior.ts
+++ b/src/mol-plugin/behavior.ts
@@ -14,7 +14,7 @@ import * as StaticMisc from './behavior/static/misc'
 import * as DynamicRepresentation from './behavior/dynamic/representation'
 import * as DynamicCamera from './behavior/dynamic/camera'
 import * as DynamicCustomProps from './behavior/dynamic/custom-props'
-import * as DynamicAnimation from './behavior/dynamic/animation'
+import * as DynamicLabels from './behavior/dynamic/labels'
 
 export const BuiltInPluginBehaviors = {
     State: StaticState,
@@ -27,5 +27,5 @@ export const PluginBehaviors = {
     Representation: DynamicRepresentation,
     Camera: DynamicCamera,
     CustomProps: DynamicCustomProps,
-    Animation: DynamicAnimation
+    Labels: DynamicLabels
 }
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts
index c463184fa10e9fe14740ae056decb8282b0c9a9e..bdfb750c2b83beaff52c5d9d8fef4cf39c6bda94 100644
--- a/src/mol-plugin/behavior/behavior.ts
+++ b/src/mol-plugin/behavior/behavior.ts
@@ -5,7 +5,7 @@
  */
 
 import { PluginStateTransform, PluginStateObject } from '../state/objects';
-import { Transformer, Transform } from 'mol-state';
+import { StateTransformer, StateTransform } from 'mol-state';
 import { Task } from 'mol-task';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommand } from '../command';
@@ -16,7 +16,7 @@ import { shallowEqual } from 'mol-util';
 export { PluginBehavior }
 
 interface PluginBehavior<P = unknown> {
-    register(ref: Transform.Ref): void,
+    register(ref: StateTransform.Ref): void,
     unregister(): void,
 
     /** Update params in place. Optionally return a promise if it depends on an async action. */
@@ -25,25 +25,54 @@ interface PluginBehavior<P = unknown> {
 
 namespace PluginBehavior {
     export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { }
+    export class Category extends PluginStateObject.Create({ name: 'Category', typeClass: 'Object' }) { }
     export class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { }
 
     export interface Ctor<P = undefined> { new(ctx: PluginContext, params: P): PluginBehavior<P> }
 
+    export const Categories = {
+        'common': 'Common',
+        'representation': 'Representation',
+        'interaction': 'Interaction',
+        'custom-props': 'Custom Properties',
+        'misc': 'Miscellaneous'
+    };
+
     export interface CreateParams<P> {
         name: string,
+        category: keyof typeof Categories,
         ctor: Ctor<P>,
+        canAutoUpdate?: StateTransformer.Definition<Root, Behavior, P>['canAutoUpdate'],
         label?: (params: P) => { label: string, description?: string },
         display: {
             name: string,
-            group: string,
             description?: string
         },
         params?(a: Root, globalCtx: PluginContext): { [K in keyof P]: ParamDefinition.Any }
     }
 
+    export type CreateCategory = typeof CreateCategory
+    export const CreateCategory = PluginStateTransform.BuiltIn({
+        name: 'create-behavior-category',
+        display: { name: 'Behavior Category' },
+        from: Root,
+        to: Category,
+        params: {
+            label: ParamDefinition.Text('', { isHidden: true }),
+        }
+    })({
+        apply({ params }) {
+            return new Category({}, { label: params.label });
+        }
+    });
+
+    const categoryMap = new Map<string, string>();
+    export function getCategoryId(t: StateTransformer) {
+        return categoryMap.get(t.id)!;
+    }
+
     export function create<P>(params: CreateParams<P>) {
-        // TODO: cache groups etc
-        return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({
+        const t = PluginStateTransform.CreateBuiltIn<Category, Behavior, P>({
             name: params.name,
             display: params.display,
             from: [Root],
@@ -55,12 +84,15 @@ namespace PluginBehavior {
             },
             update({ b, newParams }) {
                 return Task.create('Update Behavior', async () => {
-                    if (!b.data.update) return Transformer.UpdateResult.Unchanged;
+                    if (!b.data.update) return StateTransformer.UpdateResult.Unchanged;
                     const updated = await b.data.update(newParams);
-                    return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged;
+                    return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
                 })
-            }
+            },
+            canAutoUpdate: params.canAutoUpdate
         });
+        categoryMap.set(t.id, params.category);
+        return t;
     }
 
     export function simpleCommandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) {
@@ -95,7 +127,7 @@ namespace PluginBehavior {
             for (const s of this.subs) s.unsubscribe();
             this.subs = [];
         }
-        update(params: P): boolean {
+        update(params: P): boolean | Promise<boolean> {
             if (shallowEqual(params, this.params)) return false;
             this.params = params;
             return true;
@@ -103,4 +135,24 @@ namespace PluginBehavior {
         constructor(protected ctx: PluginContext, protected params: P) {
         }
     }
+
+    export abstract class WithSubscribers<P = { }> implements PluginBehavior<P> {
+        abstract register(ref: string): void;
+
+        private subs: PluginCommand.Subscription[] = [];
+        protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
+            this.subs.push(cmd.subscribe(this.plugin, action));
+        }
+        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
+            this.subs.push(o.subscribe(action));
+        }
+
+        unregister() {
+            for (const s of this.subs) s.unsubscribe();
+            this.subs = [];
+        }
+
+        constructor(protected plugin: PluginContext) {
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/animation.ts b/src/mol-plugin/behavior/dynamic/animation.ts
deleted file mode 100644
index 62a77c0dc6e6b0ccf85eedf6f8f7f5b0faec210d..0000000000000000000000000000000000000000
--- a/src/mol-plugin/behavior/dynamic/animation.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { PluginContext } from 'mol-plugin/context';
-import { PluginBehavior } from '../behavior';
-import { ParamDefinition as PD } from 'mol-util/param-definition'
-import { degToRad } from 'mol-math/misc';
-import { Mat4, Vec3 } from 'mol-math/linear-algebra';
-import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
-import { StateSelection } from 'mol-state/state/selection';
-import { StateObjectCell, State } from 'mol-state';
-
-const StructureAnimationParams = {
-    rotate: PD.Boolean(false),
-    rotateValue: PD.Numeric(0, { min: 0, max: 360, step: 0.1 }),
-    explode: PD.Boolean(false),
-    explodeValue: PD.Numeric(0, { min: 0, max: 100, step: 0.1 }),
-}
-type StructureAnimationProps = PD.Values<typeof StructureAnimationParams>
-
-function getStructure(root: StateObjectCell, state: State) {
-    const parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
-    return parent && parent.obj ? parent.obj as PluginStateObject.Molecule.Structure : undefined
-}
-
-function getRootStructure(root: StateObjectCell, state: State) {
-    let parent: StateObjectCell | undefined
-    while (true) {
-        const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
-        if (_parent) {
-            parent = _parent
-            root = _parent
-        } else {
-            break
-        }
-    }
-    return parent && parent.obj ? parent.obj as PluginStateObject.Molecule.Structure : undefined
-}
-
-/**
- * TODO
- * - animation class is just for testing purposes, needs better API
- * - allow per-unit transform `unitTransform: { [unitId: number]: Mat4 }`
- */
-export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({
-    name: 'structure-animation',
-    display: { name: 'Structure Animation', group: 'Animation' },
-    ctor: class extends PluginBehavior.Handler<StructureAnimationProps> {
-        private tmpMat = Mat4.identity()
-        private rotMat = Mat4.identity()
-        private transMat = Mat4.identity()
-        private animMat = Mat4.identity()
-        private transVec = Vec3.zero()
-        private rotVec = Vec3.create(0, 1, 0)
-
-        private rotateAnimHandle = -1
-        private explodeAnimHandle = -1
-
-        constructor(protected ctx: PluginContext, protected params: StructureAnimationProps) {
-            super(ctx, params)
-            this.update(params)
-        }
-
-        rotate(rad: number) {
-            const state = this.ctx.state.dataState
-            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
-            Mat4.rotate(this.rotMat, this.tmpMat, rad, this.rotVec)
-            for (const r of reprs) {
-                if (!SO.isRepresentation3D(r.obj)) return
-                const structure = getRootStructure(r, state)
-                if (!structure) continue
-
-                Vec3.negate(this.transVec, Vec3.copy(this.transVec, structure.data.boundary.sphere.center))
-                Mat4.fromTranslation(this.transMat, this.transVec)
-                Mat4.mul(this.animMat, this.rotMat, this.transMat)
-
-                Vec3.copy(this.transVec, structure.data.boundary.sphere.center)
-                Mat4.fromTranslation(this.transMat, this.transVec)
-                Mat4.mul(this.animMat, this.transMat, this.animMat)
-
-                r.obj.data.setState({ transform: this.animMat })
-                this.ctx.canvas3d.add(r.obj.data)
-                this.ctx.canvas3d.requestDraw(true)
-            }
-        }
-
-        animateRotate(play: boolean) {
-            if (play) {
-                const animateRotate = (t: number) => {
-                    this.rotate(degToRad((t / 10) % 360))
-                    this.rotateAnimHandle = requestAnimationFrame(animateRotate)
-                }
-                this.rotateAnimHandle = requestAnimationFrame(animateRotate)
-            } else {
-                cancelAnimationFrame(this.rotateAnimHandle)
-            }
-        }
-
-        explode(d: number) {
-            const state = this.ctx.state.dataState
-            const reprs = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Representation3D));
-            for (const r of reprs) {
-                if (!SO.isRepresentation3D(r.obj)) return
-                const structure = getStructure(r, state)
-                if (!structure) continue
-                const rootStructure = getRootStructure(r, state)
-                if (!rootStructure) continue
-
-                Vec3.sub(this.transVec, structure.data.boundary.sphere.center, rootStructure.data.boundary.sphere.center)
-                Vec3.setMagnitude(this.transVec, this.transVec, d)
-                Mat4.fromTranslation(this.animMat, this.transVec)
-
-                r.obj.data.setState({ transform: this.animMat })
-                this.ctx.canvas3d.add(r.obj.data)
-                this.ctx.canvas3d.requestDraw(true)
-            }
-        }
-
-        animateExplode(play: boolean) {
-            if (play) {
-                const animateExplode = (t: number) => {
-                    this.explode((Math.sin(t * 0.001) + 1) * 5)
-                    this.explodeAnimHandle = requestAnimationFrame(animateExplode)
-                }
-                this.explodeAnimHandle = requestAnimationFrame(animateExplode)
-            } else {
-                cancelAnimationFrame(this.explodeAnimHandle)
-            }
-        }
-
-        register(): void { }
-
-        update(p: StructureAnimationProps) {
-            let updated = PD.areEqual(StructureAnimationParams, this.params, p)
-            if (this.params.rotate !== p.rotate) {
-                this.params.rotate = p.rotate
-                this.animateRotate(this.params.rotate)
-            }
-            if (this.params.explode !== p.explode) {
-                this.params.explode = p.explode
-                this.animateExplode(this.params.explode)
-            }
-            if (this.params.rotateValue !== p.rotateValue) {
-                this.params.rotateValue = p.rotateValue
-                this.rotate(degToRad(this.params.rotateValue))
-            }
-            if (this.params.explodeValue !== p.explodeValue) {
-                this.params.explodeValue = p.explodeValue
-                this.explode(this.params.explodeValue)
-            }
-            return updated;
-        }
-
-        unregister() {
-            cancelAnimationFrame(this.rotateAnimHandle)
-            cancelAnimationFrame(this.explodeAnimHandle)
-        }
-    },
-    params: () => StructureAnimationParams
-});
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts
index 4431eec954a3fcc90d73705940c338339fbe2e23..1f5376187ad9434ec27a4554a04a8e698f552a80 100644
--- a/src/mol-plugin/behavior/dynamic/camera.ts
+++ b/src/mol-plugin/behavior/dynamic/camera.ts
@@ -7,13 +7,16 @@
 import { Loci } from 'mol-model/loci';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { PluginBehavior } from '../behavior';
+import { ButtonsType, ModifiersKeys } from 'mol-util/input/input-observer';
 
 export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({
     name: 'focus-loci-on-select',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> {
         register(): void {
-            this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {
-                if (!this.ctx.canvas3d) return;
+            this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+                if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return;
+
                 const sphere = Loci.getBoundingSphere(current.loci);
                 if (!sphere) return;
                 this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius));
@@ -21,8 +24,8 @@ export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extr
         }
     },
     params: () => ({
-        minRadius: ParamDefinition.Numeric(10, { min: 1, max: 50, step: 1 }),
+        minRadius: ParamDefinition.Numeric(8, { min: 1, max: 50, step: 1 }),
         extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the boundning sphere radius of the Loci.' })
     }),
-    display: { name: 'Focus Loci on Select', group: 'Camera' }
+    display: { name: 'Focus Loci on Select' }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
index 61a09e969723d1c0d28ece8064cee69b88f11b95..5b8bfdc183f3632ac6dccdc41119bb11b86d7663 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
@@ -9,14 +9,15 @@ import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-r
 import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report';
 import { Loci } from 'mol-model/loci';
 import { StructureElement } from 'mol-model/structure';
-import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginBehavior } from '../../../behavior';
 import { ThemeDataContext } from 'mol-theme/theme';
+import { CustomPropertyRegistry } from 'mol-model-props/common/custom-property-registry';
 
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
-    display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
+    category: 'custom-props',
+    display: { name: 'PDBe Structure Quality Report' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = StructureQualityReport.createAttachTask(
             m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
@@ -40,7 +41,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
                 factory: StructureQualityReportColorTheme,
                 getParams: () => ({}),
                 defaultValues: {},
-                isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
+                isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
             })
         }
 
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
index 3079bbcab9e21bcf2cb79c2b3f9cc4ef11bd5627..0d65bbc92b7823b7d2eea29ebc2274198f06e665 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
@@ -7,16 +7,17 @@
 import { PluginBehavior } from 'mol-plugin/behavior';
 import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry';
-import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
 import { AssemblySymmetryClusterColorThemeProvider } from 'mol-model-props/rcsb/themes/assembly-symmetry-cluster';
 import { AssemblySymmetryAxesRepresentationProvider } from 'mol-model-props/rcsb/representations/assembly-symmetry-axes';
 import { Loci, isDataLoci } from 'mol-model/loci';
 import { OrderedSet } from 'mol-data/int';
 import { Table } from 'mol-data/db';
+import { CustomPropertyRegistry } from 'mol-model-props/common/custom-property-registry';
 
 export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'rcsb-assembly-symmetry-prop',
-    display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
+    category: 'custom-props',
+    display: { name: 'RCSB Assembly Symmetry' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch);
 
diff --git a/src/mol-plugin/behavior/dynamic/labels.ts b/src/mol-plugin/behavior/dynamic/labels.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be54651502d57168464b9a568c7a69eb7043cec6
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/labels.ts
@@ -0,0 +1,233 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { PluginBehavior } from '../behavior';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { Mat4, Vec3 } from 'mol-math/linear-algebra';
+import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
+import { StateObjectCell, State, StateSelection } from 'mol-state';
+import { RuntimeContext } from 'mol-task';
+import { Shape } from 'mol-model/shape';
+import { Text } from 'mol-geo/geometry/text/text';
+import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { ColorNames } from 'mol-util/color/tables';
+import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
+import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
+import { SetUtils } from 'mol-util/set';
+import { arrayEqual } from 'mol-util';
+import { MoleculeType } from 'mol-model/structure/model/types';
+import { getElementMoleculeType } from 'mol-model/structure/util';
+
+// TODO
+// - support more object types than structures
+// - tether label to the element nearest to the bounding sphere center
+// - [Started] multiple levels of labels: structure, polymer, ligand
+// - show structure/unit label only when there is a representation with sufficient overlap
+// - support highlighting
+// - better support saccharides (use data available after re-mediation)
+// - size based on min bbox dimension (to avoid huge labels for very long but narrow polymers)
+// - fixed size labels (invariant to zoom) [needs feature in text geo]
+// - ??? max label length
+// - ??? multi line labels [needs feature in text geo]
+// - ??? use prevalent (how to define) color of representations of a structure to color the label
+// - completely different approach (render not as 3d objects): overlay free layout in screenspace with occlusion info from bboxes
+
+export type SceneLabelsLevels = 'structure' | 'polymer' | 'ligand'
+
+export const SceneLabelsParams = {
+    ...Text.Params,
+
+    background: PD.Boolean(true),
+    backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
+    backgroundColor: PD.Color(ColorNames.snow),
+    backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
+
+    levels: PD.MultiSelect([] as SceneLabelsLevels[], [
+        ['structure', 'structure'], ['polymer', 'polymer'], ['ligand', 'ligand']
+    ] as [SceneLabelsLevels, string][]),
+}
+export type SceneLabelsParams = typeof SceneLabelsParams
+export type SceneLabelsProps = PD.Values<typeof SceneLabelsParams>
+
+interface LabelsData {
+    transforms: Mat4[]
+    texts: string[]
+    positions: Vec3[]
+    sizes: number[]
+    depths: number[]
+}
+
+function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
+    const { texts, positions, depths } = data
+    const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
+    for (let i = 0, il = texts.length; i < il; ++i) {
+        const p = positions[i]
+        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
+    }
+    return textBuilder.getText()
+}
+
+export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
+    name: 'scene-labels',
+    category: 'representation',
+    display: { name: 'Scene Labels' },
+    canAutoUpdate: () => true,
+    ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
+        private data: LabelsData = {
+            transforms: [Mat4.identity()],
+            texts: [],
+            positions: [],
+            sizes: [],
+            depths: []
+        }
+        private repr: ShapeRepresentation<LabelsData, Text, SceneLabelsParams>
+        private geo = Text.createEmpty()
+        private structures = new Set<SO.Molecule.Structure>()
+
+        constructor(protected ctx: PluginContext, protected params: SceneLabelsProps) {
+            super(ctx, params)
+            this.repr = ShapeRepresentation(this.getLabelsShape, Text.Utils)
+            ctx.events.state.object.created.subscribe(this.triggerUpdate)
+            ctx.events.state.object.removed.subscribe(this.triggerUpdate)
+            ctx.events.state.object.updated.subscribe(this.triggerUpdate)
+            ctx.events.state.cell.stateUpdated.subscribe(this.triggerUpdate)
+        }
+
+        private triggerUpdate = async () => {
+            await this.update(this.params)
+        }
+
+        private getColor = () => ColorNames.dimgrey
+        private getSize = (groupId: number) => this.data.sizes[groupId]
+        private getLabel = () => ''
+
+        private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => {
+            this.geo = getLabelsText(data, props, this.geo)
+            return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms)
+        }
+
+        /** Update structures to be labeled, returns true if changed */
+        private updateStructures(p: SceneLabelsProps) {
+            const state = this.ctx.state.dataState
+            const structures = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
+            const rootStructures = new Set<SO.Molecule.Structure>()
+            for (const s of structures) {
+                const rootStructure = getRootStructure(s, state)
+                if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
+                if (!state.cellStates.get(s.transform.ref).isHidden) {
+                    rootStructures.add(rootStructure.obj)
+                }
+            }
+            if (!SetUtils.areEqual(rootStructures, this.structures)) {
+                this.structures = rootStructures
+                return true
+            } else {
+                return false
+            }
+        }
+
+        private updateLabels(p: SceneLabelsProps) {
+            const l = StructureElement.create()
+
+            const { texts, positions, sizes, depths } = this.data
+            texts.length = 0
+            positions.length = 0
+            sizes.length = 0
+            depths.length = 0
+
+            this.structures.forEach(structure => {
+                if (p.levels.includes('structure')) {
+                    texts.push(`${structure.data.model.label}`)
+                    positions.push(structure.data.boundary.sphere.center)
+                    sizes.push(structure.data.boundary.sphere.radius / 10)
+                    depths.push(structure.data.boundary.sphere.radius)
+                }
+
+                for (let i = 0, il = structure.data.units.length; i < il; ++i) {
+                    let label = ''
+                    const u = structure.data.units[i]
+                    l.unit = u
+                    l.element = u.elements[0]
+
+                    if (p.levels.includes('polymer') && u.polymerElements.length) {
+                        label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
+                    }
+
+                    if (p.levels.includes('ligand') && !u.polymerElements.length) {
+                        const moleculeType = getElementMoleculeType(u, u.elements[0])
+                        if (moleculeType === MoleculeType.other || moleculeType === MoleculeType.saccharide) {
+                            label = `${StructureProperties.entity.pdbx_description(l).join(', ')} (${getAsymId(u)(l)})`
+                        }
+                    }
+
+                    if (label) {
+                        texts.push(label)
+                        const { center, radius } = u.lookup3d.boundary.sphere
+                        const transformedCenter = Vec3.transformMat4(Vec3.zero(), center, u.conformation.operator.matrix)
+                        positions.push(transformedCenter)
+                        sizes.push(Math.max(2, radius / 10))
+                        depths.push(radius)
+                    }
+                }
+            })
+        }
+
+        register(): void { }
+
+        async update(p: SceneLabelsProps) {
+            // console.log('update')
+            let updated = false
+            if (this.updateStructures(p) || !arrayEqual(this.params.levels, p.levels)) {
+                // console.log('update with data')
+                this.updateLabels(p)
+                await this.repr.createOrUpdate(p, this.data).run()
+                updated = true
+            } else if (!PD.areEqual(SceneLabelsParams, this.params, p)) {
+                // console.log('update props only')
+                await this.repr.createOrUpdate(p).run()
+                updated = true
+            }
+            if (updated) {
+                Object.assign(this.params, p)
+                this.ctx.canvas3d.add(this.repr)
+            }
+            return updated;
+        }
+
+        unregister() {
+
+        }
+    },
+    params: () => SceneLabelsParams
+});
+
+//
+
+function getRootStructure(root: StateObjectCell, state: State) {
+    let parent: StateObjectCell | undefined
+    while (true) {
+        const _parent = StateSelection.findAncestorOfType(state.tree, state.cells, root.transform.ref, [PluginStateObject.Molecule.Structure])
+        if (_parent) {
+            parent = _parent
+            root = _parent
+        } else {
+            break
+        }
+    }
+    return parent ? parent :
+        SO.Molecule.Structure.is(root.obj) ? root : undefined
+}
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.auth_asym_id
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts
index d537242995b40f19b8a00bad5b34e93b9adb6fc4..32afe8027de6dc5f73fc03987a0fe2fea4ddb53a 100644
--- a/src/mol-plugin/behavior/dynamic/representation.ts
+++ b/src/mol-plugin/behavior/dynamic/representation.ts
@@ -1,64 +1,116 @@
 /**
- * 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>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { PluginBehavior } from '../behavior';
-import { EmptyLoci, Loci } from 'mol-model/loci';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { labelFirst } from 'mol-theme/label';
+import { EmptyLoci } from 'mol-model/loci';
+import { StructureElement } from 'mol-model/structure';
 import { PluginContext } from 'mol-plugin/context';
+import { Representation } from 'mol-repr/representation';
+import { labelFirst } from 'mol-theme/label';
+import { ButtonsType } from 'mol-util/input/input-observer';
+import { PluginBehavior } from '../behavior';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
-            let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
-            this.subscribeObservable(this.ctx.behaviors.canvas.highlightLoci, current => {
+            let prev: Representation.Loci = { loci: EmptyLoci, repr: void 0 };
+            const sel = this.ctx.helpers.structureSelection;
+
+            this.subscribeObservable(this.ctx.behaviors.canvas3d.highlight, ({ current, modifiers }) => {
                 if (!this.ctx.canvas3d) return;
 
-                if (current.repr !== prevRepr || !Loci.areEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight, prevRepr);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight, current.repr);
-                    prevLoci = current.loci;
-                    prevRepr = current.repr;
+                if (StructureElement.isLoci(current.loci)) {
+                    let loci: StructureElement.Loci = current.loci;
+                    if (modifiers && modifiers.shift) {
+                        loci = sel.tryGetRange(loci) || loci;
+                    }
+
+                    this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight);
+                    const toHighlight = { loci, repr: current.repr };
+                    this.ctx.canvas3d.mark(toHighlight, MarkerAction.Highlight);
+                    prev = toHighlight;
+                } else {
+                    if (!Representation.Loci.areEqual(prev, current)) {
+                        this.ctx.canvas3d.mark(prev, MarkerAction.RemoveHighlight);
+                        this.ctx.canvas3d.mark(current, MarkerAction.Highlight);
+                        prev = current;
+                    }
                 }
+
             });
         }
     },
-    display: { name: 'Highlight Loci on Canvas', group: 'Representation' }
+    display: { name: 'Highlight Loci on Canvas' }
 });
 
 export const SelectLoci = PluginBehavior.create({
     name: 'representation-select-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
-            let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
-            this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {
+            const sel = this.ctx.helpers.structureSelection;
+
+            const toggleSel = (current: Representation.Loci<StructureElement.Loci>) => {
+                if (sel.has(current.loci)) {
+                    sel.remove(current.loci);
+                    this.ctx.canvas3d.mark(current, MarkerAction.Deselect);
+                } else {
+                    sel.add(current.loci);
+                    this.ctx.canvas3d.mark(current, MarkerAction.Select);
+                }
+            }
+
+            this.subscribeObservable(this.ctx.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
                 if (!this.ctx.canvas3d) return;
-                if (current.repr !== prevRepr || !Loci.areEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect, prevRepr);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Select, current.repr);
-                    prevLoci = current.loci;
-                    prevRepr = current.repr;
+
+                if (current.loci.kind === 'empty-loci') {
+                    if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                        // clear the selection on Ctrl + Right-Click on empty
+                        const sels = sel.clear();
+                        for (const s of sels) this.ctx.canvas3d.mark({ loci: s }, MarkerAction.Deselect);
+                    }
+                } else if (StructureElement.isLoci(current.loci)) {
+                    if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                        // select only the current element on Ctrl + Right-Click
+                        const old = sel.get(current.loci.structure);
+                        this.ctx.canvas3d.mark({ loci: old }, MarkerAction.Deselect);
+                        sel.set(current.loci);
+                        this.ctx.canvas3d.mark(current, MarkerAction.Select);
+                    } else if (modifiers.control && buttons === ButtonsType.Flag.Primary) {
+                        // toggle current element on Ctrl + Left-Click
+                        toggleSel(current as Representation.Loci<StructureElement.Loci>);
+                    } else if (modifiers.shift && buttons === ButtonsType.Flag.Primary) {
+                        // try to extend sequence on Shift + Left-Click
+                        let loci: StructureElement.Loci = current.loci;
+                        if (modifiers && modifiers.shift) {
+                            loci = sel.tryGetRange(loci) || loci;
+                        }
+                        toggleSel({ loci, repr: current.repr });
+                    }
                 } else {
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Toggle);
+                    if (!ButtonsType.has(buttons, ButtonsType.Flag.Secondary)) return;
+                    this.ctx.canvas3d.mark(current, MarkerAction.Toggle);
                 }
-                // this.ctx.canvas3d.mark(loci, MarkerAction.Toggle);
             });
         }
     },
-    display: { name: 'Select Loci on Canvas', group: 'Representation' }
+    display: { name: 'Select Loci on Canvas' }
 });
 
 export const DefaultLociLabelProvider = PluginBehavior.create({
     name: 'default-loci-label-provider',
+    category: 'interaction',
     ctor: class implements PluginBehavior<undefined> {
         private f = labelFirst;
         register(): void { this.ctx.lociLabels.addProvider(this.f); }
         unregister() { this.ctx.lociLabels.removeProvider(this.f); }
         constructor(protected ctx: PluginContext) { }
     },
-    display: { name: 'Provide Default Loci Label', group: 'Representation' }
-});
\ No newline at end of file
+    display: { name: 'Provide Default Loci Label' }
+});
diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
new file mode 100644
index 0000000000000000000000000000000000000000..497d1420536422a93cff6f13aded50981b145507
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
@@ -0,0 +1,190 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure, StructureElement } from 'mol-model/structure';
+import { PluginBehavior } from 'mol-plugin/behavior';
+import { PluginCommands } from 'mol-plugin/command';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+import { BuiltInStructureRepresentations } from 'mol-repr/structure/registry';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
+import { StateObjectCell, StateSelection, StateTransform } from 'mol-state';
+import { BuiltInColorThemes } from 'mol-theme/color';
+import { BuiltInSizeThemes } from 'mol-theme/size';
+import { ColorNames } from 'mol-util/color/tables';
+import { ButtonsType } from 'mol-util/input/input-observer';
+import { Representation } from 'mol-repr/representation';
+
+type Params = { }
+
+enum Tags {
+    Group = 'structure-interaction-group',
+    ResidueSel = 'structure-interaction-residue-sel',
+    ResidueRepr = 'structure-interaction-residue-repr',
+    SurrSel = 'structure-interaction-surr-sel',
+    SurrRepr = 'structure-interaction-surr-repr'
+}
+
+const TagSet: Set<Tags> = new Set([Tags.Group, Tags.ResidueSel, Tags.ResidueRepr, Tags.SurrSel, Tags.SurrRepr])
+
+export class StructureRepresentationInteractionBehavior extends PluginBehavior.WithSubscribers<Params> {
+
+    private createResVisualParams(s: Structure) {
+        return StructureRepresentation3DHelpers.createParams(this.plugin, s, {
+            repr: BuiltInStructureRepresentations['ball-and-stick'],
+            size: [BuiltInSizeThemes.uniform, () => ({ value: 0.85 } )]
+        });
+    }
+
+    private createSurVisualParams(s: Structure) {
+        return StructureRepresentation3DHelpers.createParams(this.plugin, s, {
+            repr: BuiltInStructureRepresentations['ball-and-stick'],
+            color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })],
+            size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )]
+        });
+    }
+
+    private ensureShape(cell: StateObjectCell<PluginStateObject.Molecule.Structure>) {
+        const state = this.plugin.state.dataState, tree = state.tree;
+        const builder = state.build();
+        const refs = StateSelection.findUniqueTagsInSubtree(tree, cell.transform.ref, TagSet);
+
+        if (!refs['structure-interaction-group']) {
+            refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup,
+                { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref;
+        }
+
+        // Selections
+        if (!refs[Tags.ResidueSel]) {
+            refs[Tags.ResidueSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
+                { query: { } as any, label: 'Residue' }, { props: { tag: Tags.ResidueSel } }).ref;
+        }
+
+        if (!refs[Tags.SurrSel]) {
+            refs[Tags.SurrSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
+                { query: { } as any, label: 'Surroundings' }, { props: { tag: Tags.SurrSel } }).ref;
+        }
+
+        // Representations
+        // TODO: ability to customize how it looks in the behavior params
+        if (!refs[Tags.ResidueRepr]) {
+            refs[Tags.ResidueRepr] = builder.to(refs['structure-interaction-residue-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
+                this.createResVisualParams(cell.obj!.data), { props: { tag: Tags.ResidueRepr } }).ref;
+        }
+
+        if (!refs[Tags.SurrRepr]) {
+            refs[Tags.SurrRepr] = builder.to(refs['structure-interaction-surr-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
+                this.createSurVisualParams(cell.obj!.data), { props: { tag: Tags.SurrRepr } }).ref;
+        }
+
+        return { state, builder, refs };
+    }
+
+    private clear(root: StateTransform.Ref) {
+        const state = this.plugin.state.dataState;
+        const groups = state.select(StateSelection.Generators.byRef(root).subtree().filter(o => o.transform.props.tag === Tags.Group));
+        if (groups.length === 0) return;
+
+        const update = state.build();
+        const query = MS.struct.generator.empty();
+        for (const g of groups) {
+            // TODO: update props of the group node to ghost
+
+            const res = StateSelection.findTagInSubtree(state.tree, g.transform.ref, Tags.ResidueSel);
+            const surr = StateSelection.findTagInSubtree(state.tree, g.transform.ref, Tags.SurrSel);
+            if (res) update.to(res).update(StateTransforms.Model.StructureSelection, old => ({ ...old, query }));
+            if (surr) update.to(surr).update(StateTransforms.Model.StructureSelection, old => ({ ...old, query }));
+        }
+
+        PluginCommands.State.Update.dispatch(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+    }
+
+    register(ref: string): void {
+        let lastLoci: Representation.Loci = Representation.Loci.Empty;
+
+        this.subscribeObservable(this.plugin.events.state.object.removed, o => {
+            if (!PluginStateObject.Molecule.Structure.is(o.obj) || lastLoci.loci.kind !== 'element-loci') return;
+            if (lastLoci.loci.structure === o.obj.data) {
+                lastLoci = Representation.Loci.Empty;
+            }
+        });
+
+        this.subscribeObservable(this.plugin.events.state.object.updated, o => {
+            if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || lastLoci.loci.kind !== 'element-loci') return;
+            if (lastLoci.loci.structure === o.oldObj.data) {
+                lastLoci = Representation.Loci.Empty;
+            }
+        });
+
+        this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+            if (buttons !== ButtonsType.Flag.Secondary) return;
+
+            if (current.loci.kind === 'empty-loci') {
+                if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                    this.clear(StateTransform.RootRef);
+                    return;
+                }
+            }
+
+            // TODO: support link loci as well?
+            if (!StructureElement.isLoci(current.loci)) return;
+
+            const parent = this.plugin.helpers.substructureParent.get(current.loci.structure);
+            if (!parent || !parent.obj) return;
+
+            if (Representation.Loci.areEqual(lastLoci, current)) {
+                lastLoci = Representation.Loci.Empty;
+                this.clear(parent.transform.ref);
+                return;
+            }
+
+            lastLoci = current;
+
+            const core = MS.struct.modifier.wholeResidues([
+                StructureElement.Loci.toScriptExpression(current.loci)
+            ]);
+
+            const surroundings = MS.struct.modifier.includeSurroundings({
+                0: core,
+                radius: 5,
+                'as-whole-residues': true
+            });
+
+            // const surroundings = MS.struct.modifier.exceptBy({
+            //     0: MS.struct.modifier.includeSurroundings({
+            //         0: core,
+            //         radius: 5,
+            //         'as-whole-residues': true
+            //     }),
+            //     by: core
+            // });
+
+            const { state, builder, refs } = this.ensureShape(parent);
+
+            builder.to(refs[Tags.ResidueSel]!).update(StateTransforms.Model.StructureSelection, old => ({ ...old, query: core }));
+            builder.to(refs[Tags.SurrSel]!).update(StateTransforms.Model.StructureSelection, old => ({ ...old, query: surroundings }));
+
+            PluginCommands.State.Update.dispatch(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+        });
+    }
+
+    async update(params: Params) {
+        return false;
+    }
+
+    constructor(public plugin: PluginContext) {
+        super(plugin);
+    }
+}
+
+export const StructureRepresentationInteraction = PluginBehavior.create({
+    name: 'create-structure-representation-interaction',
+    display: { name: 'Structure Representation Interaction' },
+    category: 'interaction',
+    ctor: StructureRepresentationInteractionBehavior
+});
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ad87f7ff3b7a676dcd14d4d2298f06a267a7ecb
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
@@ -0,0 +1,264 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import CIF from 'mol-io/reader/cif';
+import { Box3D } from 'mol-math/geometry';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { StructureElement } from 'mol-model/structure';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
+import { PluginBehavior } from 'mol-plugin/behavior';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { createIsoValueParam } from 'mol-repr/volume/isosurface';
+import { Color } from 'mol-util/color';
+import { LRUCache } from 'mol-util/lru-cache';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { urlCombine } from 'mol-util/url';
+import { VolumeServerHeader, VolumeServerInfo } from './model';
+import { ButtonsType } from 'mol-util/input/input-observer';
+import { PluginCommands } from 'mol-plugin/command';
+import { StateSelection } from 'mol-state';
+
+export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
+
+export namespace VolumeStreaming {
+    function channelParam(label: string, color: Color, defaultValue: VolumeIsoValue, stats: VolumeData['dataStats']) {
+        return PD.Group({
+            isoValue: createIsoValueParam(defaultValue, stats),
+            color: PD.Color(color),
+            opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 })
+        }, { label, isExpanded: true });
+    }
+
+    const fakeSampling: VolumeServerHeader.Sampling = {
+        byteOffset: 0,
+        rate: 1,
+        sampleCount: [1, 1, 1],
+        valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
+    };
+
+    export function createParams(data?: VolumeServerInfo.Data) {
+        // fake the info
+        const info = data || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: VolumeIsoValue.relative(0) };
+        const box = (data && data.structure.boundary.box) || Box3D.empty();
+
+        return {
+            view: PD.MappedStatic(info.kind === 'em' ? 'cell' : 'selection-box', {
+                'box': PD.Group({
+                    bottomLeft: PD.Vec3(box.min),
+                    topRight: PD.Vec3(box.max),
+                }, { description: 'Static box defined by cartesian coords.', isFlat: true }),
+                'selection-box': PD.Group({
+                    radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), { isHidden: true }),
+                }, { description: 'Box around last-interacted element.', isFlat: true }),
+                'cell': PD.Group({}),
+                // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
+            }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] }),
+            detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
+                info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string])),
+            channels: info.kind === 'em'
+                ? PD.Group({
+                    'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || VolumeIsoValue.relative(1), info.header.sampling[0].valuesInfo[0])
+                }, { isFlat: true })
+                : PD.Group({
+                    '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), VolumeIsoValue.relative(1.5), info.header.sampling[0].valuesInfo[0]),
+                    'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), VolumeIsoValue.relative(3), info.header.sampling[0].valuesInfo[1]),
+                    'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), VolumeIsoValue.relative(-3), info.header.sampling[0].valuesInfo[1]),
+                }, { isFlat: true })
+        };
+    }
+
+    type RT = typeof createParams extends (...args: any[]) => (infer T) ? T : never
+    export type Params = RT extends PD.Params ? PD.Values<RT> : {}
+
+    type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, opacity: number } }
+    type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
+
+    export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
+    export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']]
+    interface ChannelInfo {
+        data: VolumeData,
+        color: Color,
+        isoValue: VolumeIsoValue.Relative,
+        opacity: number
+    }
+    export type Channels = { [name in ChannelType]?: ChannelInfo }
+
+    export class Behavior extends PluginBehavior.WithSubscribers<Params> {
+        private cache = LRUCache.create<ChannelsData>(25);
+        public params: Params = {} as any;
+        // private ref: string = '';
+
+        channels: Channels = {}
+
+        private async queryData(box?: Box3D) {
+            let url = urlCombine(this.info.serverUrl, `${this.info.kind}/${this.info.dataId}`);
+
+            if (box) {
+                const { min: a, max: b } = box;
+                url += `/box`
+                    + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
+                    + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
+            } else {
+                url += `/cell`;
+            }
+            url += `?detail=${this.params.detailLevel}`;
+
+            let data = LRUCache.get(this.cache, url);
+            if (data) {
+                return data;
+            }
+
+            const cif = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
+            data = await this.parseCif(cif as Uint8Array);
+            if (!data) {
+                return;
+            }
+
+            LRUCache.set(this.cache, url, data);
+            return data;
+        }
+
+        private async parseCif(data: Uint8Array): Promise<ChannelsData | undefined> {
+            const parsed = await this.plugin.runTask(CIF.parseBinary(data));
+            if (parsed.isError) {
+                this.plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
+                return;
+            }
+            if (parsed.result.blocks.length < 2) {
+                this.plugin.log.error('VolumeStreaming: Invalid data.');
+                return;
+            }
+
+            const ret: ChannelsData = {};
+            for (let i = 1; i < parsed.result.blocks.length; i++) {
+                const block = parsed.result.blocks[i];
+
+                const densityServerCif = CIF.schema.densityServer(block);
+                const volume = await this.plugin.runTask(await volumeFromDensityServerData(densityServerCif));
+                (ret as any)[block.header as any] = volume;
+            }
+            return ret;
+        }
+
+        private updateDynamicBox(ref: string, box: Box3D) {
+            if (this.params.view.name !== 'selection-box') return;
+
+            const state = this.plugin.state.dataState;
+            const newParams: Params = {
+                ...this.params,
+                view: {
+                    name: 'selection-box' as 'selection-box',
+                    params: {
+                        radius: this.params.view.params.radius,
+                        bottomLeft: box.min,
+                        topRight: box.max
+                    }
+                }
+            };
+            const update = state.build().to(ref).update(newParams);
+
+            PluginCommands.State.Update.dispatch(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
+        }
+
+        private getStructureRoot(ref: string) {
+            return this.plugin.state.dataState.select(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+        }
+
+        register(ref: string): void {
+            // this.ref = ref;
+
+            this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => {
+                if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return;
+
+                if (current.loci.kind === 'empty-loci') {
+                    if (modifiers.control && buttons === ButtonsType.Flag.Secondary) {
+                        this.updateDynamicBox(ref, Box3D.empty());
+                        return;
+                    }
+                }
+
+                // TODO: support link loci as well?
+                // Perhaps structure loci too?
+                if (!StructureElement.isLoci(current.loci)) return;
+
+                const parent = this.plugin.helpers.substructureParent.get(current.loci.structure);
+                if (!parent) return;
+                const root = this.getStructureRoot(ref);
+                if (!root || !root.obj || root.obj !== parent.obj) return;
+
+                const loci = StructureElement.Loci.extendToWholeResidues(current.loci);
+                const box = StructureElement.Loci.getBoundary(loci).box;
+                this.updateDynamicBox(ref, box);
+            });
+        }
+
+        async update(params: Params) {
+            this.params = params;
+
+            let box: Box3D | undefined = void 0, emptyData = false;
+
+            switch (params.view.name) {
+                case 'box':
+                    box = Box3D.create(params.view.params.bottomLeft, params.view.params.topRight);
+                    emptyData = Box3D.volume(box) < 0.0001;
+                    break;
+                case 'selection-box': {
+                    box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
+                    const r = params.view.params.radius;
+                    emptyData = Box3D.volume(box) < 0.0001;
+                    Box3D.expand(box, box, Vec3.create(r, r, r));
+                    break;
+                }
+                case 'cell':
+                    box = this.info.kind === 'x-ray'
+                        ? this.info.structure.boundary.box
+                        : void 0;
+                    break;
+            }
+
+            const data = emptyData ? {} : await this.queryData(box);
+
+            if (!data) return false;
+
+            const info = params.channels as ChannelsInfo;
+
+            if (this.info.kind === 'x-ray') {
+                this.channels['2fo-fc'] = this.createChannel(data['2FO-FC'] || VolumeData.One, info['2fo-fc'], this.info.header.sampling[0].valuesInfo[0]);
+                this.channels['fo-fc(+ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(+ve)'], this.info.header.sampling[0].valuesInfo[1]);
+                this.channels['fo-fc(-ve)'] = this.createChannel(data['FO-FC'] || VolumeData.One, info['fo-fc(-ve)'], this.info.header.sampling[0].valuesInfo[1]);
+            } else {
+                this.channels['em'] = this.createChannel(data['EM'] || VolumeData.One, info['em'], this.info.header.sampling[0].valuesInfo[0]);
+            }
+
+            return true;
+        }
+
+        private createChannel(data: VolumeData, info: ChannelsInfo['em'], stats: VolumeData['dataStats']): ChannelInfo {
+            const i = info!;
+            return {
+                data,
+                color: i.color,
+                opacity: i.opacity,
+                isoValue: i.isoValue.kind === 'relative' ? i.isoValue : VolumeIsoValue.toRelative(i.isoValue, stats)
+            };
+        }
+
+        getDescription() {
+            if (this.params.view.name === 'selection-box') return 'Selection';
+            if (this.params.view.name === 'box') return 'Static Box';
+            if (this.params.view.name === 'cell') return 'Cell';
+            return '';
+        }
+
+        constructor(public plugin: PluginContext, public info: VolumeServerInfo.Data) {
+            super(plugin);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba54b666d9f091c8ae407c3fb6d8ff4ad09a2398
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/model.ts
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateObject } from '../../../state/objects';
+import { VolumeIsoValue } from 'mol-model/volume';
+import { Structure } from 'mol-model/structure';
+
+export class VolumeServerInfo extends PluginStateObject.Create<VolumeServerInfo.Data>({ name: 'Volume Streaming', typeClass: 'Object' }) { }
+
+export namespace VolumeServerInfo {
+    export type Kind = 'x-ray' | 'em'
+    export interface Data  {
+        serverUrl: string,
+        kind: Kind,
+        // for em, the EMDB access code, for x-ray, the PDB id
+        dataId: string,
+        header: VolumeServerHeader,
+        emDefaultContourLevel?: VolumeIsoValue,
+        structure: Structure
+    }
+}
+
+export interface VolumeServerHeader {
+    /** Format version number  */
+    formatVersion: string,
+
+    /** Axis order from the slowest to fastest moving, same as in CCP4 */
+    axisOrder: number[],
+
+    /** Origin in fractional coordinates, in axisOrder */
+    origin: number[],
+
+    /** Dimensions in fractional coordinates, in axisOrder */
+    dimensions: number[],
+
+    spacegroup: VolumeServerHeader.Spacegroup,
+    channels: string[],
+
+    /** Determines the data type of the values */
+    valueType: VolumeServerHeader.ValueType,
+
+    /** The value are stored in blockSize^3 cubes */
+    blockSize: number,
+    sampling: VolumeServerHeader.Sampling[],
+
+    /** Precision data the server can show. */
+    availablePrecisions: VolumeServerHeader.DetailLevel[],
+
+    isAvailable: boolean
+}
+
+export namespace VolumeServerHeader {
+    export type ValueType = 'float32' | 'int8'
+
+    export namespace ValueType {
+        export const Float32: ValueType = 'float32';
+        export const Int8: ValueType = 'int8';
+    }
+
+    export type ValueArray = Float32Array | Int8Array
+
+    export type DetailLevel = { precision: number, maxVoxels: number }
+
+    export interface Spacegroup {
+        number: number,
+        size: number[],
+        angles: number[],
+        /** Determine if the data should be treated as periodic or not. (e.g. X-ray = periodic, EM = not periodic) */
+        isPeriodic: boolean,
+    }
+
+    export interface ValuesInfo {
+        mean: number,
+        sigma: number,
+        min: number,
+        max: number
+    }
+
+    export interface Sampling {
+        byteOffset: number,
+
+        /** How many values along each axis were collapsed into 1 */
+        rate: number,
+        valuesInfo: ValuesInfo[],
+
+        /** Number of samples along each axis, in axisOrder  */
+        sampleCount: number[]
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..42e01f59695ca66d0bcf036babc2364823cd0036
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateObject as SO, PluginStateTransform } from '../../../state/objects';
+import { VolumeServerInfo, VolumeServerHeader } from './model';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Task } from 'mol-task';
+import { PluginContext } from 'mol-plugin/context';
+import { urlCombine } from 'mol-util/url';
+import { createIsoValueParam } from 'mol-repr/volume/isosurface';
+import { VolumeIsoValue } from 'mol-model/volume';
+import { StateAction, StateObject, StateTransformer } from 'mol-state';
+import { getStreamingMethod, getEmdbIdAndContourLevel } from './util';
+import { VolumeStreaming } from './behavior';
+import { VolumeRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry';
+import { createTheme } from 'mol-theme/theme';
+import { Box3D } from 'mol-math/geometry';
+import { Vec3 } from 'mol-math/linear-algebra';
+// import { PluginContext } from 'mol-plugin/context';
+
+export const InitVolumeStreaming = StateAction.build({
+    display: { name: 'Volume Streaming' },
+    from: SO.Molecule.Structure,
+    params(a) {
+        return {
+            method: PD.Select<VolumeServerInfo.Kind>(getStreamingMethod(a && a.data), [['em', 'EM'], ['x-ray', 'X-Ray']]),
+            id: PD.Text((a && a.data.models.length > 0 && a.data.models[0].label) || ''),
+            serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer')
+        };
+    },
+    isApplicable: (a) => a.data.models.length === 1
+})(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => {
+    // TODO: custom react view for this and the VolumeStreamingBehavior transformer
+
+    let dataId = params.id.toLowerCase(), emDefaultContourLevel: number | undefined;
+    if (params.method === 'em') {
+        await taskCtx.update('Getting EMDB info...');
+        const emInfo = await getEmdbIdAndContourLevel(plugin, taskCtx, dataId);
+        dataId = emInfo.emdbId;
+        emDefaultContourLevel = emInfo.contour;
+    }
+
+    const infoTree = state.build().to(ref)
+        .apply(CreateVolumeStreamingInfo, {
+            serverUrl: params.serverUrl,
+            source: params.method === 'em'
+                ? { name: 'em', params: { isoValue: VolumeIsoValue.absolute(emDefaultContourLevel || 0) } }
+                : { name: 'x-ray', params: { } },
+            dataId
+        });
+
+    const infoObj = await state.updateTree(infoTree).runInContext(taskCtx);
+
+    const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior,
+        PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data)));
+
+    if (params.method === 'em') {
+        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } });
+    } else {
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+    }
+    await state.updateTree(behTree).runInContext(taskCtx);
+}));
+
+export const BoxifyVolumeStreaming = StateAction.build({
+    display: { name: 'Boxify Volume Streaming', description: 'Make the current box permanent.' },
+    from: VolumeStreaming,
+    isApplicable: (a) => a.data.params.view.name === 'selection-box'
+})(({ a, ref, state }, plugin: PluginContext) => {
+    const params = a.data.params;
+    if (params.view.name !== 'selection-box') return;
+    const box = Box3D.create(Vec3.clone(params.view.params.bottomLeft), Vec3.clone(params.view.params.topRight));
+    const r = params.view.params.radius;
+    Box3D.expand(box, box, Vec3.create(r, r, r));
+    const newParams: VolumeStreaming.Params = {
+        ...params,
+        view: {
+            name: 'box' as 'box',
+            params: {
+                bottomLeft: box.min,
+                topRight: box.max
+            }
+        }
+    };
+    return state.updateTree(state.build().to(ref).update(newParams));
+});
+
+export { CreateVolumeStreamingInfo }
+type CreateVolumeStreamingInfo = typeof CreateVolumeStreamingInfo
+const CreateVolumeStreamingInfo = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-info',
+    display: { name: 'Volume Streaming Info' },
+    from: SO.Molecule.Structure,
+    to: VolumeServerInfo,
+    params(a) {
+        return {
+            serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'),
+            source: PD.MappedStatic('x-ray', {
+                'em': PD.Group({
+                    isoValue: createIsoValueParam(VolumeIsoValue.relative(1))
+                }),
+                'x-ray': PD.Group({ })
+            }),
+            dataId: PD.Text('')
+        };
+    }
+})({
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('', async taskCtx => {
+        const dataId = params.dataId;
+        const emDefaultContourLevel = params.source.name === 'em' ? params.source.params.isoValue : VolumeIsoValue.relative(1);
+        await taskCtx.update('Getting server header...');
+        const header = await plugin.fetch<VolumeServerHeader>({ url: urlCombine(params.serverUrl, `${params.source.name}/${dataId}`), type: 'json' }).runInContext(taskCtx);
+        const data: VolumeServerInfo.Data = {
+            serverUrl: params.serverUrl,
+            dataId,
+            kind: params.source.name,
+            header,
+            emDefaultContourLevel,
+            structure: a.data
+        };
+        return new VolumeServerInfo(data, { label: `Volume Server: ${dataId}` });
+    })
+});
+
+export { CreateVolumeStreamingBehavior }
+type CreateVolumeStreamingBehavior = typeof CreateVolumeStreamingBehavior
+const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-behavior',
+    display: { name: 'Volume Streaming Behavior' },
+    from: VolumeServerInfo,
+    to: VolumeStreaming,
+    params(a) {
+        return VolumeStreaming.createParams(a && a.data);
+    }
+})({
+    canAutoUpdate: ({ oldParams, newParams }) => {
+        return oldParams.view === newParams.view
+            || (oldParams.view.name === newParams.view.name && oldParams.view.name === 'selection-box');
+    },
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
+        const behavior = new VolumeStreaming.Behavior(plugin, a.data);
+        await behavior.update(params);
+        return new VolumeStreaming(behavior, { label: 'Volume Streaming', description: behavior.getDescription() });
+    }),
+    update({ b, newParams }) {
+        return Task.create('Update Volume Streaming', async _ => {
+            const ret = await b.data.update(newParams) ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
+            b.description = b.data.getDescription();
+            return ret;
+        });
+    }
+});
+
+export { VolumeStreamingVisual }
+type VolumeStreamingVisual = typeof VolumeStreamingVisual
+const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
+    name: 'create-volume-streaming-visual',
+    display: { name: 'Volume Streaming Visual' },
+    from: VolumeStreaming,
+    to: SO.Volume.Representation3D,
+    params: {
+        channel: PD.Select<VolumeStreaming.ChannelType>('em', VolumeStreaming.ChannelTypeOptions, { isHidden: true })
+    }
+})({
+    apply: ({ a, params: srcParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        const channel = a.data.channels[srcParams.channel];
+        if (!channel) return StateObject.Null;
+
+        const params = createVolumeProps(a.data, srcParams.channel);
+
+        const provider = BuiltInVolumeRepresentations.isosurface;
+        const props = params.type.params || {}
+        const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
+        repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
+        await repr.createOrUpdate(props, channel.data).runInContext(ctx);
+        return new SO.Volume.Representation3D({ repr, source: a }, { label: `${Math.round(channel.isoValue.relativeValue * 100) / 100} σ [${srcParams.channel}]` });
+    }),
+    update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
+
+        const channel = a.data.channels[newParams.channel];
+        // TODO: is this correct behavior?
+        if (!channel) return StateTransformer.UpdateResult.Unchanged;
+
+        const params = createVolumeProps(a.data, newParams.channel);
+        const props = { ...b.data.repr.props, ...params.type.params };
+        b.data.repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: channel.data }, params))
+        await b.data.repr.createOrUpdate(props, channel.data).runInContext(ctx);
+        return StateTransformer.UpdateResult.Updated;
+    })
+});
+
+function createVolumeProps(streaming: VolumeStreaming.Behavior, channelName: VolumeStreaming.ChannelType) {
+    const channel = streaming.channels[channelName]!;
+    return VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.plugin, 'isosurface',
+        { isoValue: channel.isoValue, alpha: channel.opacity }, 'uniform', { value: channel.color });
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d3a4dbf87ff3177cad8fd51a36877b5bd01f9724
--- /dev/null
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/util.ts
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { VolumeServerInfo } from './model';
+import { PluginContext } from 'mol-plugin/context';
+import { RuntimeContext } from 'mol-task';
+
+export function getStreamingMethod(s?: Structure, defaultKind: VolumeServerInfo.Kind = 'x-ray'): VolumeServerInfo.Kind {
+    if (!s) return defaultKind;
+
+    const model = s.models[0];
+    if (model.sourceData.kind !== 'mmCIF') return defaultKind;
+
+    const data = model.sourceData.data.exptl.method;
+    for (let i = 0; i < data.rowCount; i++) {
+        const v = data.value(i).toUpperCase();
+        if (v.indexOf('MICROSCOPY') >= 0) return 'em';
+    }
+    return 'x-ray';
+}
+
+export async function getEmdbIdAndContourLevel(plugin: PluginContext, taskCtx: RuntimeContext, pdbId: string) {
+    // TODO: parametrize to a differnt URL? in plugin settings perhaps
+    const summary = await plugin.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${pdbId}`, type: 'json' }).runInContext(taskCtx);
+
+    const summaryEntry = summary && summary[pdbId];
+    let emdbId: string;
+    if (summaryEntry && summaryEntry[0] && summaryEntry[0].related_structures) {
+        const emdb = summaryEntry[0].related_structures.filter((s: any) => s.resource === 'EMDB');
+        if (!emdb.length) {
+            throw new Error(`No related EMDB entry found for '${pdbId}'.`);
+        }
+        emdbId = emdb[0].accession;
+    } else {
+        throw new Error(`No related EMDB entry found for '${pdbId}'.`);
+    }
+
+    // TODO: parametrize to a differnt URL? in plugin settings perhaps
+    const emdb = await plugin.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/emdb/entry/map/${emdbId}`, type: 'json' }).runInContext(taskCtx);
+    const emdbEntry = emdb && emdb[emdbId];
+    let contour: number | undefined = void 0;
+    if (emdbEntry && emdbEntry[0] && emdbEntry[0].map && emdbEntry[0].map.contour_level && emdbEntry[0].map.contour_level.value !== void 0) {
+        contour = +emdbEntry[0].map.contour_level.value;
+    }
+
+    return { emdbId, contour };
+}
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/static/misc.ts b/src/mol-plugin/behavior/static/misc.ts
index aff031d5bd5b855955024898797cec2cb409d626..8a400f1ca42dcdb5866d6fee6e9e48a37e9d46c9 100644
--- a/src/mol-plugin/behavior/static/misc.ts
+++ b/src/mol-plugin/behavior/static/misc.ts
@@ -14,6 +14,6 @@ export function registerDefault(ctx: PluginContext) {
 export function Canvas3DSetSettings(ctx: PluginContext) {
     PluginCommands.Canvas3D.SetSettings.subscribe(ctx, e => {
         ctx.canvas3d.setProps(e.settings);
-        ctx.events.canvad3d.settingsUpdated.next();
+        ctx.events.canvas3d.settingsUpdated.next();
     })
 }
diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts
index bf58413b59e2d4d75998175dc1a5867944148e32..b84697c9d7237d9ec824fba879929dfdc19f7eca 100644
--- a/src/mol-plugin/behavior/static/representation.ts
+++ b/src/mol-plugin/behavior/static/representation.ts
@@ -11,47 +11,82 @@ import { State } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
+    SyncStructureRepresentation3DState(ctx); // should be AFTER SyncRepresentationToCanvas
     UpdateRepresentationVisibility(ctx);
 }
 
 export function SyncRepresentationToCanvas(ctx: PluginContext) {
+    let reprCount = 0;
+
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        updateVisibility(e, e.obj.data);
-        e.obj.data.setState({ syncManually: true });
-        ctx.canvas3d.add(e.obj.data);
-        // TODO: only do this if there were no representations previously
-        ctx.canvas3d.resetCamera();
+        updateVisibility(e, e.obj.data.repr);
+        e.obj.data.repr.setState({ syncManually: true });
+        ctx.canvas3d.add(e.obj.data.repr);
+
+        if (reprCount === 0) ctx.canvas3d.resetCamera();
+        reprCount++;
     });
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
-            ctx.canvas3d.remove(e.oldObj.data);
+            ctx.canvas3d.remove(e.oldObj.data.repr);
             ctx.canvas3d.requestDraw(true);
-            e.oldObj.data.destroy();
+            e.oldObj.data.repr.destroy();
         }
 
-        if (!SO.isRepresentation3D(e.obj)) return;
+        if (!SO.isRepresentation3D(e.obj)) {
+            return;
+        }
 
-        updateVisibility(e, e.obj.data);
+        updateVisibility(e, e.obj.data.repr);
         if (e.action === 'recreate') {
-            e.obj.data.setState({ syncManually: true });
+            e.obj.data.repr.setState({ syncManually: true });
         }
-        ctx.canvas3d.add(e.obj.data);
+        ctx.canvas3d.add(e.obj.data.repr);
     });
     events.object.removed.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        ctx.canvas3d.remove(e.obj.data);
+        ctx.canvas3d.remove(e.obj.data.repr);
+        ctx.canvas3d.requestDraw(true);
+        e.obj.data.repr.destroy();
+        reprCount--;
+    });
+}
+
+
+export function SyncStructureRepresentation3DState(ctx: PluginContext) {
+    // TODO: figure out how to do transform composition here?
+    const events = ctx.state.dataState.events;
+    events.object.created.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.state);
+        ctx.canvas3d.update(data.source.data.repr);
+        ctx.canvas3d.requestDraw(true);
+    });
+    events.object.updated.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.state);
+        ctx.canvas3d.update(data.source.data.repr);
+        ctx.canvas3d.requestDraw(true);
+    });
+    events.object.removed.subscribe(e => {
+        if (!SO.Molecule.Structure.Representation3DState.is(e.obj)) return;
+        const data = e.obj.data as SO.Molecule.Structure.Representation3DStateData;
+        data.source.data.repr.setState(data.initialState);
+        ctx.canvas3d.update(data.source.data.repr);
         ctx.canvas3d.requestDraw(true);
-        e.obj.data.destroy();
     });
 }
 
+
 export function UpdateRepresentationVisibility(ctx: PluginContext) {
     ctx.state.dataState.events.cell.stateUpdated.subscribe(e => {
         const cell = e.state.cells.get(e.ref)!;
         if (!SO.isRepresentation3D(cell.obj)) return;
-        updateVisibility(e, cell.obj.data);
+        updateVisibility(e, cell.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
     })
 }
diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts
index af9541687760557e436bb8c23116f7b78b2bbfc1..818a14252d4b8f0a553c8c73ed13f30b17bd71c1 100644
--- a/src/mol-plugin/behavior/static/state.ts
+++ b/src/mol-plugin/behavior/static/state.ts
@@ -6,11 +6,11 @@
 
 import { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
-import { StateTree, Transform, State } from 'mol-state';
+import { StateTree, StateTransform, State } from 'mol-state';
 import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
-import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
-import { EmptyLoci, EveryLoci } from 'mol-model/loci';
-import { Structure } from 'mol-model/structure';
+import { PluginStateObject as SO } from '../../state/objects';
+import { getFormattedTime } from 'mol-util/date';
+import { readFromFile } from 'mol-util/data-source';
 
 export function registerDefault(ctx: PluginContext) {
     SyncBehaviors(ctx);
@@ -49,17 +49,35 @@ export function SetCurrentObject(ctx: PluginContext) {
 }
 
 export function Update(ctx: PluginContext) {
-    PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.update(tree)));
+    PluginCommands.State.Update.subscribe(ctx, ({ state, tree, options }) => ctx.runTask(state.updateTree(tree, options)));
 }
 
 export function ApplyAction(ctx: PluginContext) {
-    PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.apply(action.action, action.params, ref)));
+    PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.applyAction(action.action, action.params, ref)));
 }
 
 export function RemoveObject(ctx: PluginContext) {
-    PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => {
-        const tree = state.tree.build().delete(ref).getTree();
-        return ctx.runTask(state.update(tree));
+    function remove(state: State, ref: string) {
+        const tree = state.build().delete(ref).getTree();
+        return ctx.runTask(state.updateTree(tree));
+    }
+
+    PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref, removeParentGhosts }) => {
+        if (removeParentGhosts) {
+            const tree = state.tree;
+            let curr = tree.transforms.get(ref);
+            if (curr.parent === ref) return remove(state, ref);
+
+            while (true) {
+                const children = tree.children.get(curr.parent);
+                if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
+                const parent = tree.transforms.get(curr.parent);
+                if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref);
+                curr = parent;
+            }
+        } else {
+            return remove(state, ref);
+        }
     });
 }
 
@@ -71,11 +89,11 @@ export function ToggleVisibility(ctx: PluginContext) {
     PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
 }
 
-function setVisibility(state: State, root: Transform.Ref, value: boolean) {
+function setVisibility(state: State, root: StateTransform.Ref, value: boolean) {
     StateTree.doPreOrder(state.tree, state.transforms.get(root), { state, value }, setVisibilityVisitor);
 }
 
-function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State, value: boolean }) {
+function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state: State, value: boolean }) {
     ctx.state.updateCellState(t.ref, { isHidden: ctx.value });
 }
 
@@ -84,19 +102,19 @@ function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State
 // TODO should also work for volumes and shapes
 export function Highlight(ctx: PluginContext) {
     PluginCommands.State.Highlight.subscribe(ctx, ({ state, ref }) => {
-        const cell = state.select(ref)[0]
-        const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined
-        if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) {
-            ctx.behaviors.canvas.highlightLoci.next({ loci: Structure.Loci(cell.obj.data) })
-        } else if (repr) {
-            ctx.behaviors.canvas.highlightLoci.next({ loci: EveryLoci, repr })
-        }
+        // const cell = state.select(ref)[0]
+        // const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined
+        // if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) {
+        //     ctx.behaviors.canvas3d.highlight.next({ current: { loci: Structure.Loci(cell.obj.data) } });
+        // } else if (repr) {
+        //     ctx.behaviors.canvas3d.highlight.next({ current: { loci: EveryLoci, repr } });
+        // }
     });
 }
 
 export function ClearHighlight(ctx: PluginContext) {
     PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => {
-        ctx.behaviors.canvas.highlightLoci.next({ loci: EmptyLoci })
+        // ctx.behaviors.canvas3d.highlight.next({ current: { loci: EmptyLoci } });
     });
 }
 
@@ -109,29 +127,58 @@ export function Snapshots(ctx: PluginContext) {
         ctx.state.snapshots.remove(id);
     });
 
-    PluginCommands.State.Snapshots.Add.subscribe(ctx, ({ name, description }) => {
-        const entry = PluginStateSnapshotManager.Entry(ctx.state.getSnapshot(), name, description);
+    PluginCommands.State.Snapshots.Add.subscribe(ctx, ({ name, description, params }) => {
+        const entry = PluginStateSnapshotManager.Entry(ctx.state.getSnapshot(params), { name, description });
         ctx.state.snapshots.add(entry);
     });
 
+    PluginCommands.State.Snapshots.Replace.subscribe(ctx, ({ id, params }) => {
+        ctx.state.snapshots.replace(id, ctx.state.getSnapshot(params));
+    });
+
+    PluginCommands.State.Snapshots.Move.subscribe(ctx, ({ id, dir }) => {
+        ctx.state.snapshots.move(id, dir);
+    });
+
     PluginCommands.State.Snapshots.Apply.subscribe(ctx, ({ id }) => {
-        const e = ctx.state.snapshots.getEntry(id);
-        return ctx.state.setSnapshot(e.snapshot);
+        const snapshot = ctx.state.snapshots.setCurrent(id);
+        if (!snapshot) return;
+        return ctx.state.setSnapshot(snapshot);
     });
 
-    PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, serverUrl }) => {
+    PluginCommands.State.Snapshots.Upload.subscribe(ctx, ({ name, description, playOnLoad, serverUrl }) => {
         return fetch(`${serverUrl}/set?name=${encodeURIComponent(name || '')}&description=${encodeURIComponent(description || '')}`, {
             method: 'POST',
             mode: 'cors',
             referrer: 'no-referrer',
             headers: { 'Content-Type': 'application/json; charset=utf-8' },
-            body: JSON.stringify(ctx.state.getSnapshot())
+            body: JSON.stringify(ctx.state.snapshots.getRemoteSnapshot({ name, description, playOnLoad }))
         }) as any as Promise<void>;
     });
 
     PluginCommands.State.Snapshots.Fetch.subscribe(ctx, async ({ url }) => {
-        const req = await fetch(url, { referrer: 'no-referrer' });
-        const json = await req.json();
-        return ctx.state.setSnapshot(json.data);
+        const json = await ctx.runTask(ctx.fetch({ url, type: 'json' })); //  fetch(url, { referrer: 'no-referrer' });
+        await ctx.state.snapshots.setRemoteSnapshot(json.data);
+    });
+
+    PluginCommands.State.Snapshots.DownloadToFile.subscribe(ctx, ({ name }) => {
+        const element = document.createElement('a');
+        const json = JSON.stringify(ctx.state.getSnapshot(), null, 2);
+        element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(json));
+        element.setAttribute('download', `mol-star_state_${(name || getFormattedTime())}.json`);
+        element.style.display = 'none';
+        document.body.appendChild(element);
+        element.click();
+        document.body.removeChild(element);
+    });
+
+    PluginCommands.State.Snapshots.OpenFile.subscribe(ctx, async ({ file }) => {
+        try {
+            const data = await readFromFile(file, 'string').run();
+            const snapshot = JSON.parse(data as string);
+            return ctx.state.setSnapshot(snapshot);
+        } catch (e) {
+            ctx.log.error(`Reading JSON state: ${e}`);
+        }
     });
 }
\ No newline at end of file
diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts
index 211e70870c8e93e48729cb72aa6d03caf10a4512..05a967dccdd398e0e8815993742a515da3a2a221 100644
--- a/src/mol-plugin/command.ts
+++ b/src/mol-plugin/command.ts
@@ -6,46 +6,62 @@
 
 import { Camera } from 'mol-canvas3d/camera';
 import { PluginCommand } from './command/base';
-import { Transform, State } from 'mol-state';
-import { StateAction } from 'mol-state/action';
+import { StateTransform, State, StateAction } from 'mol-state';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
+import { PluginLayoutStateProps } from './layout';
+import { StructureElement } from 'mol-model/structure';
+import { PluginState } from './state';
 
 export * from './command/base';
 
 export const PluginCommands = {
     State: {
-        SetCurrentObject: PluginCommand<{ state: State, ref: Transform.Ref }>(),
-        ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>(),
-        Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder }>(),
+        SetCurrentObject: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: StateTransform.Ref }>(),
+        Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder, options?: Partial<State.UpdateOptions> }>(),
 
-        RemoveObject: PluginCommand<{ state: State, ref: Transform.Ref }>(),
+        RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(),
 
-        ToggleExpanded: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        ToggleVisibility: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        Highlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        ClearHighlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
+        ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
 
         Snapshots: {
-            Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),
-            Remove: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Apply: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Clear: PluginCommand<{}>({ isImmediate: true }),
+            Add: PluginCommand<{ name?: string, description?: string, params?: PluginState.GetSnapshotParams }>(),
+            Replace: PluginCommand<{ id: string, params?: PluginState.GetSnapshotParams }>(),
+            Move: PluginCommand<{ id: string, dir: -1 | 1 }>(),
+            Remove: PluginCommand<{ id: string }>(),
+            Apply: PluginCommand<{ id: string }>(),
+            Clear: PluginCommand<{}>(),
 
-            Upload: PluginCommand<{ name?: string, description?: string, serverUrl: string }>({ isImmediate: true }),
-            Fetch: PluginCommand<{ url: string }>()
+            Upload: PluginCommand<{ name?: string, description?: string, playOnLoad?: boolean, serverUrl: string }>(),
+            Fetch: PluginCommand<{ url: string }>(),
+
+            DownloadToFile: PluginCommand<{ name?: string }>(),
+            OpenFile: PluginCommand<{ file: File }>(),
+        }
+    },
+    Interactivity: {
+        Structure: {
+            Highlight: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>(),
+            Select: PluginCommand<{ loci: StructureElement.Loci, isOff?: boolean }>()
         }
     },
+    Layout: {
+        Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>()
+    },
     Camera: {
-        Reset: PluginCommand<{}>({ isImmediate: true }),
-        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }),
+        Reset: PluginCommand<{}>(),
+        SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(),
         Snapshots: {
-            Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),
-            Remove: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Apply: PluginCommand<{ id: string }>({ isImmediate: true }),
-            Clear: PluginCommand<{}>({ isImmediate: true }),
+            Add: PluginCommand<{ name?: string, description?: string }>(),
+            Remove: PluginCommand<{ id: string }>(),
+            Apply: PluginCommand<{ id: string }>(),
+            Clear: PluginCommand<{}>(),
         }
     },
     Canvas3D: {
-        SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>({ isImmediate: true })
+        SetSettings: PluginCommand<{ settings: Partial<Canvas3DProps> }>()
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/command/base.ts b/src/mol-plugin/command/base.ts
index fcf1980a4e158b1dc29f27a7c085b5c187327095..70aca58427031bc88899b0e454839e4be830f30b 100644
--- a/src/mol-plugin/command/base.ts
+++ b/src/mol-plugin/command/base.ts
@@ -5,8 +5,6 @@
  */
 
 import { PluginContext } from '../context';
-import { LinkedList } from 'mol-data/generic';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { UUID } from 'mol-util';
 
 export { PluginCommand }
@@ -14,24 +12,23 @@ export { PluginCommand }
 interface PluginCommand<T = unknown> {
     readonly id: UUID,
     dispatch(ctx: PluginContext, params: T): Promise<void>,
-    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
-    params: { isImmediate: boolean }
+    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription
 }
 
 /** namespace.id must a globally unique identifier */
-function PluginCommand<T>(params?: Partial<PluginCommand<T>['params']>): PluginCommand<T> {
-    return new Impl({ isImmediate: false, ...params });
+function PluginCommand<T>(): PluginCommand<T> {
+    return new Impl();
 }
 
 class Impl<T> implements PluginCommand<T> {
     dispatch(ctx: PluginContext, params: T): Promise<void> {
-        return ctx.commands.dispatch(this, params)
+        return ctx.commands.dispatch(this, params);
     }
     subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
         return ctx.commands.subscribe(this, action);
     }
     id = UUID.create22();
-    constructor(public params: PluginCommand<T>['params']) {
+    constructor() {
     }
 }
 
@@ -42,24 +39,13 @@ namespace PluginCommand {
         unsubscribe(): void
     }
 
-    export type Action<T> = (params: T) => void | Promise<void>
+    export type Action<T> = (params: T) => unknown | Promise<unknown>
     type Instance = { cmd: PluginCommand<any>, params: any, resolve: () => void, reject: (e: any) => void }
 
     export class Manager {
         private subs = new Map<string, Action<any>[]>();
-        private queue = LinkedList<Instance>();
         private disposing = false;
 
-        private ev = RxEventHelper.create();
-
-        readonly behaviour = {
-            locked: this.ev.behavior<boolean>(false)
-        };
-
-        lock(locked: boolean = true) {
-            this.behaviour.locked.next(locked);
-        }
-
         subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription {
             let actions = this.subs.get(cmd.id);
             if (!actions) {
@@ -97,37 +83,22 @@ namespace PluginCommand {
                     return;
                 }
 
-                const instance: Instance = { cmd, params, resolve, reject };
-
-                if (cmd.params.isImmediate) {
-                    this.resolve(instance);
-                } else {
-                    this.queue.addLast({ cmd, params, resolve, reject });
-                    this.next();
-                }
+                this.resolve({ cmd, params, resolve, reject });
             });
         }
 
         dispose() {
             this.subs.clear();
-            while (this.queue.count > 0) {
-                this.queue.removeFirst();
-            }
         }
 
         private async resolve(instance: Instance) {
             const actions = this.subs.get(instance.cmd.id);
             if (!actions) {
-                try {
-                    instance.resolve();
-                } finally {
-                    if (!instance.cmd.params.isImmediate && !this.disposing) this.next();
-                }
+                instance.resolve();
                 return;
             }
 
             try {
-                if (!instance.cmd.params.isImmediate) this.executing = true;
                 // TODO: should actions be called "asynchronously" ("setImmediate") instead?
                 for (const a of actions) {
                     await a(instance.params);
@@ -135,19 +106,7 @@ namespace PluginCommand {
                 instance.resolve();
             } catch (e) {
                 instance.reject(e);
-            } finally {
-                if (!instance.cmd.params.isImmediate) {
-                    this.executing = false;
-                    if (!this.disposing) this.next();
-                }
             }
         }
-
-        private executing = false;
-        private async next() {
-            if (this.queue.count === 0 || this.executing) return;
-            const instance = this.queue.removeFirst()!;
-            this.resolve(instance);
-        }
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/component.ts b/src/mol-plugin/component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b49f80c1bb2d219fe334bbbf9b093bf37e66fa1e
--- /dev/null
+++ b/src/mol-plugin/component.ts
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { shallowMergeArray } from 'mol-util/object';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
+
+export class PluginComponent<State> {
+    private _ev: RxEventHelper;
+
+    protected get ev() {
+        return this._ev || (this._ev = RxEventHelper.create());
+    }
+
+    private _state: State;
+
+    protected updateState(...states: Partial<State>[]): boolean {
+        const latest = this.state;
+        const s = shallowMergeArray(latest, states);
+        if (s !== latest) {
+            this._state = s;
+            return true;
+        }
+        return false;
+    }
+
+    get state() {
+        return this._state;
+    }
+
+    dispose() {
+        if (this._ev) this._ev.dispose();
+    }
+
+    constructor(initialState: State) {
+        this._state = initialState;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 069805cb01d623126d81116f43fd632e0449076e..6b2e8e6e44db466294b3d80b08593d3ff86472b9 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -4,27 +4,36 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { List } from 'immutable';
 import { Canvas3D } from 'mol-canvas3d/canvas3d';
-import { EmptyLoci, Loci } from 'mol-model/loci';
-import { Representation } from 'mol-repr/representation';
+import { CustomPropertyRegistry } from 'mol-model-props/common/custom-property-registry';
 import { StructureRepresentationRegistry } from 'mol-repr/structure/registry';
-import { State, Transform, Transformer } from 'mol-state';
+import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
+import { State, StateTransform, StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
 import { ColorTheme } from 'mol-theme/color';
 import { SizeTheme } from 'mol-theme/size';
 import { ThemeRegistryContext } from 'mol-theme/theme';
+import { Color } from 'mol-util/color';
+import { ajaxGet } from 'mol-util/data-source';
 import { LogEntry } from 'mol-util/log-entry';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { merge } from 'rxjs';
 import { BuiltInPluginBehaviors } from './behavior';
+import { PluginBehavior } from './behavior/behavior';
 import { PluginCommand, PluginCommands } from './command';
+import { PluginLayout } from './layout';
 import { PluginSpec } from './spec';
 import { PluginState } from './state';
-import { TaskManager } from './util/task-manager';
-import { Color } from 'mol-util/color';
+import { DataFormatRegistry } from './state/actions/data-format';
+import { StateTransformParameters } from './ui/state/common';
 import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager';
-import { ajaxGet } from 'mol-util/data-source';
-import { CustomPropertyRegistry } from './util/custom-prop-registry';
+import { TaskManager } from './util/task-manager';
+import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
+import { StructureElementSelectionManager } from './util/structure-element-selection';
+import { SubstructureParentHelper } from './util/substructure-parent-helper';
+import { Representation } from 'mol-repr/representation';
+import { ModifiersKeys } from 'mol-util/input/input-observer';
 
 export class PluginContext {
     private disposed = false;
@@ -51,23 +60,27 @@ export class PluginContext {
         },
         log: this.ev<LogEntry>(),
         task: this.tasks.events,
-        labels: {
-            highlight: this.ev<{ entries: ReadonlyArray<LociLabelEntry> }>()
-        },
-        canvad3d: {
+        canvas3d: {
             settingsUpdated: this.ev()
         }
     };
 
     readonly behaviors = {
-        canvas: {
-            highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
-            selectLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
+        state: {
+            isAnimating: this.ev.behavior<boolean>(false),
+            isUpdating: this.ev.behavior<boolean>(false)
         },
-        command: this.commands.behaviour
+        canvas3d: {
+            highlight: this.ev.behavior<Canvas3D.HighlightEvent>({ current: Representation.Loci.Empty, prev: Representation.Loci.Empty }),
+            click: this.ev.behavior<Canvas3D.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0 })
+        },
+        labels: {
+            highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })
+        }
     };
 
     readonly canvas3d: Canvas3D;
+    readonly layout: PluginLayout = new PluginLayout(this);
 
     readonly lociLabels: LociLabelManager;
 
@@ -76,10 +89,27 @@ export class PluginContext {
         themeCtx: { colorThemeRegistry: ColorTheme.createRegistry(), sizeThemeRegistry: SizeTheme.createRegistry() } as ThemeRegistryContext
     }
 
+    readonly volumeRepresentation = {
+        registry: new VolumeRepresentationRegistry(),
+        themeCtx: { colorThemeRegistry: ColorTheme.createRegistry(), sizeThemeRegistry: SizeTheme.createRegistry() } as ThemeRegistryContext
+    }
+
+    readonly dataFormat = {
+        registry: new DataFormatRegistry()
+    }
+
     readonly customModelProperties = new CustomPropertyRegistry();
+    readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
+
+    readonly helpers = {
+        structureSelection: new StructureElementSelectionManager(this),
+        substructureParent: new SubstructureParentHelper(this)
+    };
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
+            this.layout.setRoot(container);
+            if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             this.canvas3d.animate();
@@ -92,6 +122,7 @@ export class PluginContext {
     }
 
     readonly log = {
+        entries: List<LogEntry>(),
         entry: (e: LogEntry) => this.events.log.next(e),
         error: (msg: string) => this.events.log.next(LogEntry.error(msg)),
         message: (msg: string) => this.events.log.next(LogEntry.message(msg)),
@@ -103,11 +134,7 @@ export class PluginContext {
      * This should be used in all transform related request so that it could be "spoofed" to allow
      * "static" access to resources.
      */
-    fetch(url: string, type: 'string' | 'binary' = 'string', body?: string): Task<string | Uint8Array> {
-        return ajaxGet({ url, type, body });
-        // const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' });
-        // return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
-    }
+    readonly fetch = ajaxGet
 
     runTask<T>(task: Task<T>) {
         return this.tasks.run(task);
@@ -120,9 +147,26 @@ export class PluginContext {
         this.ev.dispose();
         this.state.dispose();
         this.tasks.dispose();
+        this.layout.dispose();
         this.disposed = true;
     }
 
+    applyTransform(state: State, a: StateTransform.Ref, transformer: StateTransformer, params: any) {
+        const tree = state.build().to(a).apply(transformer, params);
+        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    }
+
+    updateTransform(state: State, a: StateTransform.Ref, params: any) {
+        const tree = state.build().to(a).update(params);
+        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    }
+
+    private initBehaviorEvents() {
+        merge(this.state.dataState.events.isUpdating, this.state.behaviorState.events.isUpdating).subscribe(u => {
+            this.behaviors.state.isUpdating.next(u);
+        });
+    }
+
     private initBuiltInBehavior() {
         BuiltInPluginBehaviors.State.registerDefault(this);
         BuiltInPluginBehaviors.Representation.registerDefault(this);
@@ -132,40 +176,54 @@ export class PluginContext {
         merge(this.state.dataState.events.log, this.state.behaviorState.events.log).subscribe(e => this.events.log.next(e));
     }
 
-    async initBehaviors() {
-        const tree = this.state.behaviorState.tree.build();
+    private async initBehaviors() {
+        const tree = this.state.behaviorState.build();
+
+        for (const cat of Object.keys(PluginBehavior.Categories)) {
+            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } });
+        }
 
         for (const b of this.spec.behaviors) {
-            tree.toRoot().apply(b.transformer, b.defaultParams || { }, { ref: b.transformer.id });
+            tree.to(PluginBehavior.getCategoryId(b.transformer)).apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
         }
 
-        await this.runTask(this.state.behaviorState.update(tree));
+        await this.runTask(this.state.behaviorState.updateTree(tree, { doNotUpdateCurrent: true, doNotLogTiming: true }));
     }
 
-    initDataActions() {
+    private initDataActions() {
         for (const a of this.spec.actions) {
             this.state.dataState.actions.add(a.action);
         }
     }
 
-    applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) {
-        const tree = state.tree.build().to(a).apply(transformer, params);
-        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    private initAnimations() {
+        if (!this.spec.animations) return;
+        for (const anim of this.spec.animations) {
+            this.state.animation.register(anim);
+        }
     }
 
-    updateTransform(state: State, a: Transform.Ref, params: any) {
-        const tree = state.build().to(a).update(params);
-        return PluginCommands.State.Update.dispatch(this, { state, tree });
+    private initCustomParamEditors() {
+        if (!this.spec.customParamEditors) return;
+
+        for (const [t, e] of this.spec.customParamEditors) {
+            this.customParamEditors.set(t.id, e);
+        }
     }
 
     constructor(public spec: PluginSpec) {
+        this.events.log.subscribe(e => this.log.entries = this.log.entries.push(e));
+
+        this.initBehaviorEvents();
         this.initBuiltInBehavior();
 
         this.initBehaviors();
         this.initDataActions();
+        this.initAnimations();
+        this.initCustomParamEditors();
 
         this.lociLabels = new LociLabelManager(this);
-    }
 
-    // settings = ;
+        this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts
index c645fe4f75ffe9afe6a2e44f8669bde598b20449..3fce9342c9409b2ce783bb4559a0f3b8de0aa507 100644
--- a/src/mol-plugin/index.ts
+++ b/src/mol-plugin/index.ts
@@ -9,57 +9,67 @@ import { PluginContext } from './context';
 import { Plugin } from './ui/plugin'
 import * as React from 'react';
 import * as ReactDOM from 'react-dom';
-import { PluginCommands } from './command';
 import { PluginSpec } from './spec';
-import {DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenPlyFile} from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
+import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode } from './state/animation/built-in';
+import { StateActions } from './state/actions';
+import { InitVolumeStreaming, BoxifyVolumeStreaming, CreateVolumeStreamingBehavior } from './behavior/dynamic/volume-streaming/transformers';
+import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction';
 
-function getParam(name: string, regex: string): string {
-    let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
-    return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
-}
-
-const DefaultSpec: PluginSpec = {
+export const DefaultPluginSpec: PluginSpec = {
     actions: [
-        PluginSpec.Action(DownloadStructure),
-        PluginSpec.Action(OpenStructure),
-        PluginSpec.Action(OpenPlyFile),
-        PluginSpec.Action(CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.DownloadStructure),
+        PluginSpec.Action(StateActions.Volume.DownloadDensity),
+        PluginSpec.Action(StateActions.DataFormat.OpenFile),
+        PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.EnableModelCustomProps),
+
+        // Volume streaming
+        PluginSpec.Action(InitVolumeStreaming),
+        PluginSpec.Action(BoxifyVolumeStreaming),
+        PluginSpec.Action(CreateVolumeStreamingBehavior),
+
         PluginSpec.Action(StateTransforms.Data.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
+        PluginSpec.Action(StateTransforms.Data.ParseCcp4),
+        PluginSpec.Action(StateTransforms.Data.ParseDsn6),
+        PluginSpec.Action(StateTransforms.Model.TrajectoryFromMmCif),
+        PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB),
         PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
+        PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
-        PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D)
+        PluginSpec.Action(StateTransforms.Model.UserStructureSelection),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),
+        PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.StructureLabels3D),
+        PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.ColorStructureRepresentation3D),
+        PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
+
+        PluginSpec.Action(StateActions.Structure.StructureFromSelection),
     ],
     behaviors: [
         PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
-        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
-        PluginSpec.Behavior(PluginBehaviors.Animation.StructureAnimation, { rotate: false, rotateValue: 0, explode: false, explodeValue: 0 }),
+        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 8, extraRadius: 4 }),
+        // PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
+        PluginSpec.Behavior(StructureRepresentationInteraction)
+    ],
+    animations: [
+        AnimateModelIndex,
+        AnimateAssemblyUnwind,
+        AnimateUnitsExplode,
     ]
 }
 
-export function createPlugin(target: HTMLElement): PluginContext {
-    const ctx = new PluginContext(DefaultSpec);
+export function createPlugin(target: HTMLElement, spec?: PluginSpec): PluginContext {
+    const ctx = new PluginContext(spec || DefaultPluginSpec);
     ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target);
-
-    trySetSnapshot(ctx);
-
     return ctx;
-}
-
-async function trySetSnapshot(ctx: PluginContext) {
-    try {
-        const snapshotUrl = getParam('snapshot-url', `[^&]+`);
-        if (!snapshotUrl) return;
-        await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl })
-    } catch (e) {
-        ctx.log.error('Failed to load snapshot.');
-        console.warn('Failed to load snapshot', e);
-    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/layout.ts b/src/mol-plugin/layout.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3be770d7036fa5db618f7c72d8bbf9f088d616e5
--- /dev/null
+++ b/src/mol-plugin/layout.ts
@@ -0,0 +1,191 @@
+/**
+ * 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';
+import { PluginComponent } from './component';
+import { PluginContext } from './context';
+import { PluginCommands } from './command';
+
+// TODO: support collapsed state control orientation
+export const PluginLayoutStateParams = {
+    isExpanded: PD.Boolean(false),
+    showControls: PD.Boolean(true)
+}
+
+export type PluginLayoutStateProps = PD.Values<typeof PluginLayoutStateParams>
+
+interface RootState {
+    top: string | null,
+    bottom: string | null,
+    left: string | null,
+    right: string | null,
+
+    width: string | null,
+    height: string | null,
+    maxWidth: string | null,
+    maxHeight: string | null,
+    margin: string | null,
+    marginLeft: string | null,
+    marginRight: string | null,
+    marginTop: string | null,
+    marginBottom: string | null,
+
+    scrollTop: number,
+    scrollLeft: number,
+    position: string | null,
+    overflow: string | null,
+    viewports: HTMLElement[],
+    zindex: string | null
+}
+
+export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
+    readonly events = {
+        updated: this.ev()
+    }
+
+    private updateProps(state: Partial<PluginLayoutStateProps>) {
+        let prevExpanded = !!this.state.isExpanded;
+        this.updateState(state);
+        if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
+
+        this.events.updated.next();
+    }
+
+    private root: HTMLElement;
+    private rootState: RootState | undefined = void 0;
+    private expandedViewport: HTMLMetaElement;
+
+    setProps(props: Partial<PluginLayoutStateProps>) {
+        this.updateState(props);
+    }
+
+    setRoot(root: HTMLElement) {
+        this.root = root;
+        if (this.state.isExpanded) this.handleExpand();
+    }
+
+    private getScrollElement() {
+        if ((document as any).scrollingElement) return (document as any).scrollingElement;
+        if (document.documentElement) return document.documentElement;
+        return document.body;
+    }
+
+    private handleExpand() {
+        try {
+            let body = document.getElementsByTagName('body')[0];
+            let head = document.getElementsByTagName('head')[0];
+
+            if (!body || !head) return;
+
+            if (this.state.isExpanded) {
+                let children = head.children;
+                let hasExp = false;
+                let viewports: HTMLElement[] = [];
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        hasExp = true;
+                    } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
+                        viewports.push(children[i] as any);
+                    }
+                }
+
+                for (let v of viewports) {
+                    head.removeChild(v);
+                }
+
+                if (!hasExp) head.appendChild(this.expandedViewport);
+
+
+                let s = body.style;
+
+                let doc = this.getScrollElement();
+                let scrollLeft = doc.scrollLeft;
+                let scrollTop = doc.scrollTop;
+
+                this.rootState = {
+                    top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zindex: this.root.style.zIndex,
+                    width: s.width, height: s.height,
+                    maxWidth: s.maxWidth, maxHeight: s.maxHeight,
+                    margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
+                };
+
+                s.overflow = 'hidden';
+                s.position = 'fixed';
+                s.top = '0';
+                s.bottom = '0';
+                s.right = '0';
+                s.left = '0';
+
+                s.width = '100%';
+                s.height = '100%';
+                s.maxWidth = '100%';
+                s.maxHeight = '100%';
+                s.margin = '0';
+                s.marginLeft = '0';
+                s.marginRight = '0';
+                s.marginTop = '0';
+                s.marginBottom = '0';
+
+                // TODO: setting this breaks viewport controls for some reason. Is there a fix?
+                // this.root.style.zIndex = '100000';
+            } else {
+                let children = head.children;
+                for (let i = 0; i < children.length; i++) {
+                    if (children[i] === this.expandedViewport) {
+                        head.removeChild(this.expandedViewport);
+                        break;
+                    }
+                }
+
+                if (this.rootState) {
+                    let s = body.style, t = this.rootState;
+                    for (let v of t.viewports) {
+                        head.appendChild(v);
+                    }
+                    s.top = t.top;
+                    s.bottom = t.bottom;
+                    s.left = t.left;
+                    s.right = t.right;
+
+                    s.width = t.width;
+                    s.height = t.height;
+                    s.maxWidth = t.maxWidth;
+                    s.maxHeight = t.maxHeight;
+                    s.margin = t.margin;
+                    s.marginLeft = t.marginLeft;
+                    s.marginRight = t.marginRight;
+                    s.marginTop = t.marginTop;
+                    s.marginBottom = t.marginBottom;
+
+                    s.position = t.position;
+                    s.overflow = t.overflow;
+                    let doc = this.getScrollElement();
+                    doc.scrollTop = t.scrollTop;
+                    doc.scrollLeft = t.scrollLeft;
+                    this.rootState = void 0;
+                    this.root.style.zIndex = t.zindex;
+                }
+            }
+        } catch (e) {
+            this.context.log.error('Layout change error, you might have to reload the page.');
+            console.log('Layout change error, you might have to reload the page.', e);
+        }
+    }
+
+    constructor(private context: PluginContext) {
+        super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...(context.spec.layout && context.spec.layout.initial) });
+
+        PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
+
+        // TODO how best make sure it runs on node.js as well as in the browser?
+        if (typeof document !== 'undefined') {
+            // <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
+            this.expandedViewport = document.createElement('meta') as any;
+            this.expandedViewport.name = 'viewport';
+            this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/providers/custom-prop.ts b/src/mol-plugin/providers/custom-prop.ts
deleted file mode 100644
index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000
--- a/src/mol-plugin/providers/custom-prop.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO
\ No newline at end of file
diff --git a/src/mol-plugin/providers/theme.ts b/src/mol-plugin/providers/theme.ts
deleted file mode 100644
index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000
--- a/src/mol-plugin/providers/theme.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO
\ No newline at end of file
diff --git a/src/mol-plugin/skin/base/components/controls-base.scss b/src/mol-plugin/skin/base/components/controls-base.scss
index 4bd3f6c21b1314a4d4daefe728338d306b86e74c..89270c23ea34c4771530e6f16f73ac36c14296aa 100644
--- a/src/mol-plugin/skin/base/components/controls-base.scss
+++ b/src/mol-plugin/skin/base/components/controls-base.scss
@@ -11,11 +11,30 @@
 }
 
 .msp-btn-icon {
+    border: none;
     height: $row-height;
     width: $row-height;
     line-height: $row-height;
     padding: 0;
     text-align: center;
+
+    &[disabled], &[disabled]:hover, &[disabled]:active {
+        color: $msp-btn-link-toggle-off-font-color;
+    }
+}
+
+.msp-btn-icon-small {
+    border: none;
+    height: $row-height;
+    width: 20px;
+    font-size: 85%;
+    line-height: $row-height;
+    padding: 0;
+    text-align: center;
+
+    &[disabled], &[disabled]:hover, &[disabled]:active {
+        color: $msp-btn-link-toggle-off-font-color;
+    }
 }
 
 .msp-btn-link {
@@ -109,6 +128,10 @@
     }
 }
 
+.msp-control-top-offset {
+    margin-top: 1px;
+}
+
 .msp-btn-commit {        
     text-align: right;
     padding-top: 0;
@@ -123,10 +146,8 @@
     .msp-icon {
         display: block-inline;
         line-height: $row-height;
-        margin-right: $control-spacing;
         width: $row-height;
         text-align: center;
-        float: left;
     } 
 }
 
diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss
index 31474759cc641cf90c6d4057bd704b551ec518a3..bd527f69b3c6fa427c0ebe08921aa5539b5374b2 100644
--- a/src/mol-plugin/skin/base/components/controls.scss
+++ b/src/mol-plugin/skin/base/components/controls.scss
@@ -76,19 +76,10 @@
     > div:first-child {
         position: absolute;
         top: 0;
-        left: 0;
+        left: 18px;
         bottom: 0;
-        right: 50px;
-        width: 100%;
-        padding-right: 50px;
-        display: table;
-        
-        > div {
-            height: $row-height;
-            display: table-cell;
-            vertical-align: middle;
-            padding: 0 ($control-spacing + 4px);
-        }
+        right: 62px;
+        display: grid;
     }
     > div:last-child {
         position: absolute;
@@ -101,9 +92,12 @@
         bottom: 0;
     }
     
-    // input[type=text] {
-    //     text-align: right;
-    // }
+    input[type=text] {
+        padding-right: 6px;
+        padding-left: 4px;
+        font-size: 80%;
+        text-align: right;
+    }
     
     // input[type=range] {
     //     width: 100%;
@@ -125,20 +119,10 @@
     > div:nth-child(2) {
         position: absolute;
         top: 0;
-        left: 0;
+        left: 35px;
         bottom: 0;
-        right: 25px;
-        width: 100%;
-        padding-left: 20px;
-        padding-right: 25px;
-        display: table;
-        
-        > div {
-            height: $row-height;
-            display: table-cell;
-            vertical-align: middle;
-            padding: 0 ($control-spacing + 4px);
-        }
+        right: 37px;
+        display: grid;
     }
     > div:last-child {
         position: absolute;
@@ -152,9 +136,12 @@
         font-size: 80%;
     }
     
-    // input[type=text] {
-    //     text-align: right;
-    // }
+    input[type=text] {
+        padding-right: 4px;
+        padding-left: 4px;
+        font-size: 80%;
+        text-align: center;
+    }
     
     // input[type=range] {
     //     width: 100%;
@@ -218,7 +205,7 @@
     //border-left-style: solid;
     //border-left-color: color-increase-contrast($default-background, 10%);
 
-    margin-bottom: 1px;
+    margin-bottom: 0px;
     padding-top: 1px;
 }
 
diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss
index d11284bcf2feee85acc1e334685872c053da564d..db173d4fcd351d6081a260b76a4d5ee6def2c62f 100644
--- a/src/mol-plugin/skin/base/components/misc.scss
+++ b/src/mol-plugin/skin/base/components/misc.scss
@@ -66,4 +66,8 @@
     background: white;
     cursor: inherit;
     display: block;
+}
+
+.msp-contols-section {
+    margin-bottom: $control-spacing;
 }
\ No newline at end of file
diff --git a/src/mol-plugin/skin/base/components/slider.scss b/src/mol-plugin/skin/base/components/slider.scss
index 3d879558045cb147effefb5861d7eb6e457c57c2..cc5c2c689c48808b44927d8b015600e9d29c90e4 100644
--- a/src/mol-plugin/skin/base/components/slider.scss
+++ b/src/mol-plugin/skin/base/components/slider.scss
@@ -14,6 +14,7 @@
   padding: 5px 0;
   width: 100%;
   border-radius: $slider-border-radius-base;
+  align-self: center;
   @include borderBox;
 
   &-rail {
diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss
index 84624ff7e3e050ed85c8ed67f911b260419f572c..8913fd5d31ccf465be9388fe3aa63820d001eac7 100644
--- a/src/mol-plugin/skin/base/components/temp.scss
+++ b/src/mol-plugin/skin/base/components/temp.scss
@@ -6,12 +6,27 @@
     height: $row-height;
     line-height: $row-height;
     margin-bottom: $control-spacing;
-    text-align: center;
+    text-align: right;
+    padding: 0 $control-spacing;
     font-weight: bold;
     background: $default-background;
     // border-right: $control-spacing solid $entity-color-Group; // TODO separate color
-    border-top: 1px solid $entity-color-Group; // TODO separate color
+    border-bottom: 1px solid $entity-color-Group; // TODO separate color
     // border-bottom: 1px solid $entity-color-Group; // TODO separate color
+
+    > .msp-icon {
+        display: block;
+        float: left;
+    }
+}
+
+.msp-current-header {
+    height: $row-height;
+    line-height: $row-height;
+    margin-bottom: $control-spacing;
+    text-align: center;
+    font-weight: bold;
+    background: $default-background;
 }
 
 .msp-btn-row-group {
@@ -43,6 +58,12 @@
         > button:first-child {
             border-left: $control-spacing solid color-increase-contrast($default-background, 12%) !important;
         }
+
+        > div {
+            position: absolute;
+            right: 0;
+            top: 0;        
+        }
     }
 
     button {
@@ -51,13 +72,6 @@
     }
 }
 
-.msp-state-list-remove-button {
-    position: absolute;
-    right: 0;
-    top: 0;
-    width: $row-height;
-}
-
 .msp-tree-row {
     position: relative;
     height: $row-height;
@@ -69,10 +83,32 @@
     margin-bottom: 1px;
     padding-left: $row-height;
     padding-right: 2 * $row-height + $control-spacing;
-    border-bottom-left-radius: $control-spacing;
+    border-left: 1px solid $entity-color-Group; // TODO custom color
+    // border-bottom-left-radius: $control-spacing;
 
     &-current {
-        background: $control-background
+        // background: $control-background;
+        
+        a {
+            color: $font-color;
+        }
+
+        a:hover, a:hover > small {
+            color: color-lower-contrast($font-color, 24%);
+        }
+    }
+
+    a {
+        display: block;
+    }
+
+    a > small {
+        color: $font-color;
+    }
+
+    a:hover {
+        font-weight: bold;
+        text-decoration: none;
     }
 }
 
@@ -90,6 +126,7 @@
     left: 0;
     top: 0;
     width: $row-height;
+    padding: 0;
     color: color-lower-contrast($font-color, 24%);
 }
 
@@ -117,4 +154,79 @@
         margin-bottom: 1px;
         padding: 3px 6px;
     }
+}
+
+.msp-viewport-top-left-controls {
+    position: absolute;
+    left: $control-spacing;
+    top: $control-spacing;
+
+    .msp-traj-controls {        
+        line-height: $row-height;
+        float: left;
+        margin-right: $control-spacing;
+        background-color: $msp-form-control-background;
+
+        > span {
+            color: $font-color;
+            margin-left: $control-spacing;
+            margin-right: $control-spacing;
+            font-size: 85%;
+            display: inline-block;
+        }
+    }
+
+    .msp-state-snapshot-viewport-controls {
+        line-height: $row-height;
+        float: left;
+        margin-right: $control-spacing;
+        background-color: $msp-form-control-background;
+
+        > select {
+            display: inline-block;
+            width: 200px;
+        }
+    }
+
+    .msp-animation-viewport-controls {
+        line-height: $row-height;
+        float: left;
+        margin-right: $control-spacing;
+        position: relative;
+        background-color: $msp-form-control-background;
+
+        .msp-animation-viewport-controls-select {
+            width: 290px;
+            position: absolute;
+            left: 0;
+            top: $row-height + $control-spacing;
+            background: $control-background;
+
+            .msp-control-row:first-child {
+                margin-top: 0;
+            }
+        }
+    }
+}
+
+.msp-param-object-list-item {
+    margin-top: 1px;
+    position: relative;
+    > button {
+        text-align: left;
+        > span {
+            font-weight: bold;
+        }
+    }
+    > div {
+        position: absolute;
+        right: 0;
+        top: 0
+    }
+}
+
+.msp-state-actions {
+    .msp-transform-wrapper:last-child {
+        margin-bottom: 10px;
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/skin/base/components/transformer.scss b/src/mol-plugin/skin/base/components/transformer.scss
index aca44d435bbafbec2138e5da13b8974509273875..8fe043e01d0b0739bdf60b9592e20aa7019e1d16 100644
--- a/src/mol-plugin/skin/base/components/transformer.scss
+++ b/src/mol-plugin/skin/base/components/transformer.scss
@@ -10,7 +10,7 @@
     }
 }
 
-.msp-layout-right {
+.msp-layout-right, .msp-layout-left {
     background: $control-background;
 }
 
@@ -42,14 +42,48 @@
     margin-bottom: $control-spacing;
 }
 
+.msp-transform-wrapper-collapsed {
+    margin-bottom: 1px;
+}
+
+.msp-transform-update-wrapper {
+    margin-bottom: 1px;
+}
+
+.msp-transform-update-wrapper-collapsed {
+    margin-bottom: 1px;
+}
+
+.msp-transform-update-wrapper, .msp-transform-update-wrapper-collapsed {
+    > .msp-transform-header > button {
+        text-align: left;
+        padding-left: $row-height;
+        line-height: 24px;
+        background: color-lower-contrast($control-background, 4%); // $control-background; // color-lower-contrast($default-background, 4%);
+        // font-weight: bold;
+    }
+}
+
+.msp-transform-wrapper > .msp-transform-header > button {
+    text-align: left;
+    background: color-lower-contrast($default-background, 4%);
+    font-weight: bold;
+}
+
 .msp-transform-header {
     position: relative;
-    border-top: 1px solid $entity-color-Behaviour; // TODO: separate color
+    // border-top: 1px solid $entity-color-Behaviour; // TODO: separate color
 
-    > button {
-        text-align: left;
-        background: color-lower-contrast($default-background, 4%);
-        font-weight: bold;
+    // > button {
+    //     text-align: left;
+    //     padding-left: $row-height;
+    //     background: $control-background; // color-lower-contrast($default-background, 4%);
+    //     font-weight: bold;
+    // }
+
+    > button > small {
+        font-weight: normal;
+        float: right;
     }
 
     > button:hover {
@@ -58,9 +92,12 @@
 }
 
 .msp-transform-default-params {
+    background: $default-background;
     position: absolute;
-    right: 0;
+    left: 0;
     top: 0;
+    width: $row-height;
+    padding: 0;
 }
 
 .msp-transform-default-params:hover {
@@ -74,7 +111,8 @@
 }
 
 .msp-transform-refresh {
-    width: $control-label-width + $control-spacing;
+    width: $control-label-width + $control-spacing - $row-height - 1;
+    margin-left: $row-height + 1;
     background: $default-background;
     text-align: right;
 }
diff --git a/src/mol-plugin/skin/base/components/viewport.scss b/src/mol-plugin/skin/base/components/viewport.scss
index c8123e2f1870928fa462806de1a4d7f2bcdcd25b..228e721a5d122265db9f71a2abea5b487b666c19 100644
--- a/src/mol-plugin/skin/base/components/viewport.scss
+++ b/src/mol-plugin/skin/base/components/viewport.scss
@@ -26,6 +26,7 @@
     -webkit-user-select: none;
     -webkit-tap-highlight-color: rgba(0,0,0,0);
     -webkit-touch-callout: none;
+    touch-action: manipulation;
 }
 
 .msp-viewport-controls {
@@ -64,6 +65,10 @@
 .msp-viewport-controls-scene-options {
     width: 290px;
     background: $control-background;
+
+    .msp-control-group-wrapper:first-child {
+        padding-top: 0;
+    }
 }
 
 /* highlight */
@@ -75,9 +80,9 @@
     background: $default-background; //$highlight-info-background;
 
     position: absolute;
-    top: $control-spacing;
-    left: $control-spacing;
-    text-align: left;
+    right: $control-spacing;
+    bottom: $control-spacing;
+    text-align: right;
     min-height: $row-height;
     max-width: 95%;
 
diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss
index d79242cfc706b4831107786af0152cb6c16107cf..2c4c0590c5916943e5db8569171199a3ecd82eee 100644
--- a/src/mol-plugin/skin/base/icons.scss
+++ b/src/mol-plugin/skin/base/icons.scss
@@ -54,6 +54,10 @@
 	content: "\e812";
 }
 
+.msp-icon-back:before { 
+	content: "\e820";
+}
+
 .msp-icon-cross:before { 
 	content: "\e868";
 }
@@ -132,4 +136,80 @@
 
 .msp-icon-help-circle:before { 
 	content: "\e81d";
-}
\ No newline at end of file
+}
+
+.msp-icon-model-prev:before {
+    content: "\e884";
+}
+
+.msp-icon-model-next:before {
+    content: "\e885";
+}
+
+.msp-icon-model-first:before {
+    content: "\e89c";
+}
+
+.msp-icon-down-thin:before {
+	content: "\e88b";
+}
+
+.msp-icon-up-thin:before {
+	content: "\e88e";
+}
+
+.msp-icon-left-thin:before {
+	content: "\e88c";
+}
+
+.msp-icon-right-thin:before {
+	content: "\e88d";
+}
+
+.msp-icon-switch:before {
+	content: "\e896";
+}
+
+.msp-icon-play:before {
+	content: "\e897";
+}
+
+.msp-icon-stop:before {
+	content: "\e898";
+}
+
+.msp-icon-pause:before {
+	content: "\e899";
+}
+
+.msp-icon-left-open:before {
+	content: "\e87c";
+}
+
+.msp-icon-right-open:before {
+	content: "\e87d";
+}
+
+.msp-icon-cw:before {
+	content: "\e890";
+}
+
+.msp-icon-database:before {
+	content: "\e8d3";
+}
+
+.msp-icon-upload:before {
+	content: "\e82e";
+}
+
+.msp-icon-record:before {
+	content: "\e89a";
+}
+
+.msp-icon-code:before {
+	content: "\e834";
+}
+
+.msp-icon-floppy:before {
+	content: "\e8d0";
+}
diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts
index 40413fd87827a946700446b29e671924b0c3d7a0..6a8599df9b7870cd3b5d8b31de1b59ad89516ca5 100644
--- a/src/mol-plugin/spec.ts
+++ b/src/mol-plugin/spec.ts
@@ -4,34 +4,51 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateAction } from 'mol-state/action';
-import { Transformer } from 'mol-state';
+import { StateTransformer, StateAction } from 'mol-state';
 import { StateTransformParameters } from './ui/state/common';
+import { PluginLayoutStateProps } from './layout';
+import { PluginStateAnimation } from './state/animation/model';
 
 export { PluginSpec }
 
 interface PluginSpec {
     actions: PluginSpec.Action[],
-    behaviors: PluginSpec.Behavior[]
+    behaviors: PluginSpec.Behavior[],
+    animations?: PluginStateAnimation[],
+    customParamEditors?: [StateAction | StateTransformer, StateTransformParameters.Class][],
+    layout?: {
+        initial?: Partial<PluginLayoutStateProps>,
+        controls?: {
+            left?: React.ComponentClass | 'none',
+            right?: React.ComponentClass | 'none',
+            bottom?: React.ComponentClass | 'none'
+        }
+    }
 }
 
 namespace PluginSpec {
     export interface Action {
-        action: StateAction | Transformer,
+        action: StateAction | StateTransformer,
         customControl?: StateTransformParameters.Class,
         autoUpdate?: boolean
     }
 
-    export function Action(action: StateAction | Transformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action {
+    export function Action(action: StateAction | StateTransformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action {
         return { action, customControl: params && params.customControl, autoUpdate: params && params.autoUpdate };
     }
 
     export interface Behavior {
-        transformer: Transformer,
+        transformer: StateTransformer,
         defaultParams?: any
     }
 
-    export function Behavior<T extends Transformer>(transformer: T, defaultParams?: Transformer.Params<T>): Behavior {
+    export function Behavior<T extends StateTransformer>(transformer: T, defaultParams?: StateTransformer.Params<T>): Behavior {
         return { transformer, defaultParams };
     }
+
+    export interface LayoutControls {
+        left?: React.ComponentClass | 'none',
+        right?: React.ComponentClass | 'none',
+        bottom?: React.ComponentClass | 'none'
+    }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts
index 616538d627f787a907cdf3d720b5b5cfc254c90a..0e2a82007cb13fd299de8f7e56cf8fdd302ee28d 100644
--- a/src/mol-plugin/state.ts
+++ b/src/mol-plugin/state.ts
@@ -13,6 +13,9 @@ import { PluginStateSnapshotManager } from './state/snapshots';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginCommands } from './command';
+import { PluginAnimationManager } from './state/animation/manager';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { UUID } from 'mol-util';
 export { PluginState }
 
 class PluginState {
@@ -20,9 +23,9 @@ class PluginState {
 
     readonly dataState: State;
     readonly behaviorState: State;
+    readonly animation: PluginAnimationManager;
     readonly cameraSnapshots = new CameraSnapshotManager();
-
-    readonly snapshots = new PluginStateSnapshotManager();
+    readonly snapshots: PluginStateSnapshotManager;
 
     readonly behavior = {
         kind: this.ev.behavior<PluginState.Kind>('data'),
@@ -39,25 +42,50 @@ class PluginState {
         }
     }
 
-    getSnapshot(): PluginState.Snapshot {
+    getSnapshot(params?: PluginState.GetSnapshotParams): PluginState.Snapshot {
+        const p = { ...PluginState.DefaultGetSnapshotParams, ...params };
         return {
-            data: this.dataState.getSnapshot(),
-            behaviour: this.behaviorState.getSnapshot(),
-            cameraSnapshots: this.cameraSnapshots.getStateSnapshot(),
-            canvas3d: {
-                camera: this.plugin.canvas3d.camera.getSnapshot(),
-                viewport: this.plugin.canvas3d.props
-            }
+            id: UUID.create22(),
+            data: p.data ? this.dataState.getSnapshot() : void 0,
+            behaviour: p.behavior ? this.behaviorState.getSnapshot() : void 0,
+            animation: p.animation ? this.animation.getSnapshot() : void 0,
+            startAnimation: p.startAnimation ? !!p.startAnimation : void 0,
+            camera: p.camera ? {
+                current: this.plugin.canvas3d.camera.getSnapshot(),
+                transitionStyle: p.cameraTranstion.name,
+                transitionDurationInMs: (params && params.cameraTranstion && params.cameraTranstion.name === 'animate' && params.cameraTranstion.params.durationInMs) || void 0
+            } : void 0,
+            cameraSnapshots: p.cameraSnapshots ? this.cameraSnapshots.getStateSnapshot() : void 0,
+            canvas3d: p.canvas3d ? {
+                props: this.plugin.canvas3d.props
+            } : void 0,
+            durationInMs: params && params.durationInMs
         };
     }
 
     async setSnapshot(snapshot: PluginState.Snapshot) {
-        await this.plugin.runTask(this.behaviorState.setSnapshot(snapshot.behaviour));
-        await this.plugin.runTask(this.dataState.setSnapshot(snapshot.data));
-        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: snapshot.canvas3d.viewport || { } });
-        this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots);
-        this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
-        this.plugin.canvas3d.requestDraw(true);
+        await this.animation.stop();
+
+        if (snapshot.behaviour) await this.plugin.runTask(this.behaviorState.setSnapshot(snapshot.behaviour));
+        if (snapshot.data) await this.plugin.runTask(this.dataState.setSnapshot(snapshot.data));
+        if (snapshot.canvas3d) {
+            if (snapshot.canvas3d.props) await PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: snapshot.canvas3d.props || { } });
+        }
+        if (snapshot.cameraSnapshots) this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots);
+        if (snapshot.animation) {
+            this.animation.setSnapshot(snapshot.animation);
+        }
+        if (snapshot.camera) {
+            PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, {
+                snapshot: snapshot.camera.current,
+                durationMs: snapshot.camera.transitionStyle === 'animate'
+                    ? snapshot.camera.transitionDurationInMs
+                    : void 0
+            });
+        }
+        if (snapshot.startAnimation) {
+            this.animation.start();
+        }
     }
 
     dispose() {
@@ -65,11 +93,13 @@ class PluginState {
         this.dataState.dispose();
         this.behaviorState.dispose();
         this.cameraSnapshots.dispose();
+        this.animation.dispose();
     }
 
     constructor(private plugin: import('./context').PluginContext) {
+        this.snapshots = new PluginStateSnapshotManager(plugin);
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
 
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o);
@@ -79,19 +109,50 @@ class PluginState {
         });
 
         this.behavior.currentObject.next(this.dataState.behaviors.currentObject.value);
+
+        this.animation = new PluginAnimationManager(plugin);
     }
 }
 
 namespace PluginState {
     export type Kind = 'data' | 'behavior'
 
+    export type CameraTransitionStyle = 'instant' | 'animate'
+    export const GetSnapshotParams = {
+        durationInMs: PD.Numeric(1500, { min: 100, max: 15000, step: 100 }, { label: 'Duration in ms' }),
+        data: PD.Boolean(true),
+        behavior: PD.Boolean(false),
+        animation: PD.Boolean(true),
+        startAnimation: PD.Boolean(false),
+        canvas3d: PD.Boolean(true),
+        camera: PD.Boolean(true),
+        // TODO: make camera snapshots same as the StateSnapshots with "child states?"
+        cameraSnapshots: PD.Boolean(false),
+        cameraTranstion: PD.MappedStatic('animate', {
+            animate: PD.Group({
+                durationInMs: PD.Numeric(250, { min: 100, max: 5000, step: 500 }, { label: 'Duration in ms' }),
+            }),
+            instant: PD.Group({ })
+        }, { options: [['animate', 'Animate'], ['instant', 'Instant']] })
+    };
+    export type GetSnapshotParams = Partial<PD.Values<typeof GetSnapshotParams>>
+    export const DefaultGetSnapshotParams = PD.getDefaultValues(GetSnapshotParams);
+
     export interface Snapshot {
-        data: State.Snapshot,
-        behaviour: State.Snapshot,
-        cameraSnapshots: CameraSnapshotManager.StateSnapshot,
-        canvas3d: {
-            camera: Camera.Snapshot,
-            viewport: Canvas3DProps
-        }
+        id: UUID,
+        data?: State.Snapshot,
+        behaviour?: State.Snapshot,
+        animation?: PluginAnimationManager.Snapshot,
+        startAnimation?: boolean,
+        camera?: {
+            current: Camera.Snapshot,
+            transitionStyle: CameraTransitionStyle,
+            transitionDurationInMs?: number
+        },
+        cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
+        canvas3d?: {
+            props?: Canvas3DProps
+        },
+        durationInMs?: number
     }
 }
diff --git a/src/mol-plugin/state/actions.ts b/src/mol-plugin/state/actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5491ef5cfdcb1f0e9c4c7575c888199f9e38f4cc
--- /dev/null
+++ b/src/mol-plugin/state/actions.ts
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as Structure from './actions/structure'
+import * as Volume from './actions/volume'
+import * as DataFormat from './actions/data-format'
+
+export const StateActions = {
+    Structure,
+    Volume,
+    DataFormat
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts
deleted file mode 100644
index 969473511681136adce73d85d5e3a4f69869314e..0000000000000000000000000000000000000000
--- a/src/mol-plugin/state/actions/basic.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { PluginContext } from 'mol-plugin/context';
-import { StateTree, Transformer } from 'mol-state';
-import { StateAction } from 'mol-state/action';
-import { StateSelection } from 'mol-state/state/selection';
-import { StateTreeBuilder } from 'mol-state/tree/builder';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { PluginStateObject } from '../objects';
-import { StateTransforms } from '../transforms';
-import { Download } from '../transforms/data';
-import { StructureRepresentation3DHelpers } from '../transforms/representation';
-
-// TODO: "structure parser provider"
-
-export { DownloadStructure };
-type DownloadStructure = typeof DownloadStructure
-const DownloadStructure = StateAction.build({
-    from: PluginStateObject.Root,
-    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
-    params: {
-        source: PD.MappedStatic('bcif-static', {
-            'pdbe-updated': PD.Group({
-                id: PD.Text('1cbs', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'rcsb': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'bcif-static': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'url': PD.Group({
-                url: PD.Text(''),
-                isBinary: PD.Boolean(false),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true })
-        }, {
-            options: [
-                ['pdbe-updated', 'PDBe Updated'],
-                ['rcsb', 'RCSB'],
-                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
-                ['url', 'URL']
-            ]
-        })
-    }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const src = params.source;
-    let url: Transformer.Params<Download>;
-
-    switch (src.name) {
-        case 'url':
-            url = src.params;
-            break;
-        case 'pdbe-updated':
-            url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
-            break;
-        case 'rcsb':
-            url = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
-            break;
-        case 'bcif-static':
-            url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
-            break;
-        default: throw new Error(`${(src as any).name} not supported.`);
-    }
-
-    const data = b.toRoot().apply(StateTransforms.Data.Download, url);
-    return state.update(createStructureTree(ctx, data, params.source.params.supportProps));
-});
-
-export const OpenStructure = StateAction.build({
-    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
-    from: PluginStateObject.Root,
-    params: { file: PD.File({ accept: '.cif,.bcif' }) }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-    return state.update(createStructureTree(ctx, data, false));
-});
-
-export const OpenPlyFile = StateAction.build({
-    display: { name: 'Open PLY file', description: 'Load a PLY file' },
-    from: PluginStateObject.Root,
-    params: { file: PD.File({ accept: '.ply' }) }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const data = b.toRoot()
-        .apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: false })
-        .apply(StateTransforms.Data.ParsePly)
-        .apply(StateTransforms.Model.ShapeFromPly)
-        .apply(StateTransforms.Representation.ShapeRepresentation3D)
-    return state.update(data.getTree());
-});
-
-function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree {
-    let root = b
-        .apply(StateTransforms.Data.ParseCif)
-        .apply(StateTransforms.Model.TrajectoryFromMmCif)
-        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
-    if (supportProps) {
-        root = root.apply(StateTransforms.Model.CustomModelProperties);
-    }
-    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
-    complexRepresentation(ctx, structure);
-
-    return root.getTree();
-}
-
-function complexRepresentation(ctx: PluginContext, root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) {
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
-}
-
-export const CreateComplexRepresentation = StateAction.build({
-    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
-    from: PluginStateObject.Molecule.Structure
-})(({ ref, state }, ctx: PluginContext) => {
-    const root = state.build().to(ref);
-    complexRepresentation(ctx, root);
-    return state.update(root.getTree());
-});
-
-export const UpdateTrajectory = StateAction.build({
-    display: { name: 'Update Trajectory' },
-    params: {
-        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
-        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
-    }
-})(({ params, state }) => {
-    const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
-
-    const update = state.build();
-
-    if (params.action === 'reset') {
-        for (const m of models) {
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                () => ({ modelIndex: 0 }));
-        }
-    } else {
-        for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
-            if (!parent || !parent.obj) continue;
-            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                old => {
-                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                    if (modelIndex < 0) modelIndex += traj.data.length;
-                    return { modelIndex };
-                });
-        }
-    }
-
-    return state.update(update);
-});
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3cb8a05da5a2fae2722df766e895f691050b2d53
--- /dev/null
+++ b/src/mol-plugin/state/actions/data-format.ts
@@ -0,0 +1,159 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateBuilder, StateAction } from 'mol-state';
+import { Task } from 'mol-task';
+import { FileInfo, getFileInfo } from 'mol-util/file-info';
+import { PluginStateObject } from '../objects';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Ccp4Provider, Dsn6Provider, DscifProvider } from './volume';
+import { StateTransforms } from '../transforms';
+import { MmcifProvider, PdbProvider, GroProvider } from './structure';
+import msgpackDecode from 'mol-io/common/msgpack/decode'
+import { PlyProvider } from './shape';
+
+export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
+    private _list: { name: string, provider: DataFormatProvider<D> }[] = []
+    private _map = new Map<string, DataFormatProvider<D>>()
+    private _extensions: Set<string> | undefined = undefined
+    private _binaryExtensions: Set<string> | undefined = undefined
+    private _options: [string, string][] | undefined = undefined
+
+    get types(): [string, string][] {
+        return this._list.map(e => [e.name, e.provider.label] as [string, string]);
+    }
+
+    get extensions() {
+        if (this._extensions) return this._extensions
+        const extensions = new Set<string>()
+        this._list.forEach(({ provider }) => {
+            provider.stringExtensions.forEach(ext => extensions.add(ext))
+            provider.binaryExtensions.forEach(ext => extensions.add(ext))
+        })
+        this._extensions = extensions
+        return extensions
+    }
+
+    get binaryExtensions() {
+        if (this._binaryExtensions) return this._binaryExtensions
+        const binaryExtensions = new Set<string>()
+        this._list.forEach(({ provider }) => provider.binaryExtensions.forEach(ext => binaryExtensions.add(ext)))
+        this._binaryExtensions = binaryExtensions
+        return binaryExtensions
+    }
+
+    get options() {
+        if (this._options) return this._options
+        const options: [string, string][] = [['auto', 'Automatic']]
+        this._list.forEach(({ name, provider }) => options.push([ name, provider.label ]))
+        this._options = options
+        return options
+    }
+
+    constructor() {
+        this.add('ccp4', Ccp4Provider)
+        this.add('dscif', DscifProvider)
+        this.add('dsn6', Dsn6Provider)
+        this.add('gro', GroProvider)
+        this.add('mmcif', MmcifProvider)
+        this.add('pdb', PdbProvider)
+        this.add('ply', PlyProvider)
+    };
+
+    private _clear() {
+        this._extensions = undefined
+        this._binaryExtensions = undefined
+        this._options = undefined
+    }
+
+    add(name: string, provider: DataFormatProvider<D>) {
+        this._clear()
+        this._list.push({ name, provider })
+        this._map.set(name, provider)
+    }
+
+    remove(name: string) {
+        this._clear()
+        this._list.splice(this._list.findIndex(e => e.name === name), 1)
+        this._map.delete(name)
+    }
+
+    auto(info: FileInfo, dataStateObject: D) {
+        for (let i = 0, il = this.list.length; i < il; ++i) {
+            const { provider } = this._list[i]
+            if (provider.isApplicable(info, dataStateObject.data)) return provider
+        }
+        throw new Error('no compatible data format provider available')
+    }
+
+    get(name: string): DataFormatProvider<D> {
+        if (this._map.has(name)) {
+            return this._map.get(name)!
+        } else {
+            throw new Error(`unknown data format name '${name}'`)
+        }
+    }
+
+    get list() {
+        return this._list
+    }
+}
+
+export interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
+    label: string
+    description: string
+    stringExtensions: string[]
+    binaryExtensions: string[]
+    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
+    getDefaultBuilder(ctx: PluginContext, data: StateBuilder.To<D>, state?: State): Task<void>
+}
+
+//
+
+export const OpenFile = StateAction.build({
+    display: { name: 'Open File', description: 'Load a file and create its default visual' },
+    from: PluginStateObject.Root,
+    params: (a, ctx: PluginContext) => {
+        const { extensions, options } = ctx.dataFormat.registry
+        return {
+            file: PD.File({ accept: Array.from(extensions).map(e => `.${e}`).join(',')}),
+            format: PD.Select('auto', options),
+        }
+    }
+})(({ params, state }, ctx: PluginContext) => Task.create('Open File', async taskCtx => {
+    const info = getFileInfo(params.file)
+    const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: ctx.dataFormat.registry.binaryExtensions.has(info.ext) });
+    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
+
+    // Alternative for more complex states where the builder is not a simple StateBuilder.To<>:
+    /*
+    const dataRef = dataTree.ref;
+    await state.updateTree(dataTree).runInContext(taskCtx);
+    const dataCell = state.select(dataRef)[0];
+    */
+
+    // const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
+
+    const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(info, dataStateObject) : ctx.dataFormat.registry.get(params.format)
+    const b = state.build().to(data.ref);
+    // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
+    await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
+}));
+
+//
+
+type cifVariants = 'dscif' | -1
+export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants {
+    if (info.ext === 'bcif') {
+        try {
+            if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif'
+        } catch { }
+    } else if (info.ext === 'cif') {
+        if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif'
+    }
+    return -1
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/shape.ts b/src/mol-plugin/state/actions/shape.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7a1311149ed894a5112b189a1ab0f92f323b79d8
--- /dev/null
+++ b/src/mol-plugin/state/actions/shape.ts
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateBuilder } from 'mol-state';
+import { Task } from 'mol-task';
+import { FileInfo } from 'mol-util/file-info';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { DataFormatProvider } from './data-format';
+
+export const PlyProvider: DataFormatProvider<any> = {
+    label: 'PLY',
+    description: 'PLY',
+    stringExtensions: ['ply'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'ply'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => {
+        return Task.create('PLY default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParsePly)
+                .apply(StateTransforms.Model.ShapeFromPly)
+                .apply(StateTransforms.Representation.ShapeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b9b623d33ce48996797da7efe367930b6235cc22
--- /dev/null
+++ b/src/mol-plugin/state/actions/structure.ts
@@ -0,0 +1,306 @@
+/**
+ * Copyright (c) 2018-2019 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 { PluginContext } from 'mol-plugin/context';
+import { StateAction, StateBuilder, StateSelection, StateTransformer, State } from 'mol-state';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { Download } from '../transforms/data';
+import { StructureRepresentation3DHelpers } from '../transforms/representation';
+import { CustomModelProperties, StructureSelection } from '../transforms/model';
+import { DataFormatProvider, guessCifVariant } from './data-format';
+import { FileInfo } from 'mol-util/file-info';
+import { Task } from 'mol-task';
+import { StructureElement } from 'mol-model/structure';
+
+export const MmcifProvider: DataFormatProvider<any> = {
+    label: 'mmCIF',
+    description: 'mmCIF',
+    stringExtensions: ['cif', 'mmcif', 'mcif'],
+    binaryExtensions: ['bcif'],
+    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+        if (info.ext === 'mmcif' || info.ext === 'mcif') return true
+        // assume cif/bcif files that are not DensityServer CIF are mmCIF
+        if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif'
+        return false
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, state: State) => {
+        return Task.create('mmCIF default builder', async taskCtx => {
+            const traj = createModelTree(data, 'cif');
+            await state.updateTree(createStructureTree(ctx, traj, false)).runInContext(taskCtx)
+        })
+    }
+}
+
+export const PdbProvider: DataFormatProvider<any> = {
+    label: 'PDB',
+    description: 'PDB',
+    stringExtensions: ['pdb', 'ent'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'pdb' || info.ext === 'ent'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => {
+        return Task.create('PDB default builder', async taskCtx => {
+            const traj = createModelTree(data, 'pdb');
+            await state.updateTree(createStructureTree(ctx, traj, false)).runInContext(taskCtx)
+        })
+    }
+}
+
+export const GroProvider: DataFormatProvider<any> = {
+    label: 'GRO',
+    description: 'GRO',
+    stringExtensions: ['gro'],
+    binaryExtensions: [],
+    isApplicable: (info: FileInfo, data: string) => {
+        return info.ext === 'gro'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => {
+        return Task.create('GRO default builder', async taskCtx => {
+            const traj = createModelTree(data, 'gro');
+            await state.updateTree(createStructureTree(ctx, traj, false)).runInContext(taskCtx)
+        })
+    }
+}
+
+//
+
+const DownloadStructurePdbIdSourceOptions = PD.Group({
+    supportProps: PD.Optional(PD.Boolean(false)),
+    asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' }))
+});
+
+export { DownloadStructure };
+type DownloadStructure = typeof DownloadStructure
+const DownloadStructure = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
+    params: {
+        source: PD.MappedStatic('bcif-static', {
+            'pdbe-updated': PD.Group({
+                id: PD.Text('1cbs', { label: 'Id' }),
+                options: DownloadStructurePdbIdSourceOptions
+            }, { isFlat: true }),
+            'rcsb': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                options: DownloadStructurePdbIdSourceOptions
+            }, { isFlat: true }),
+            'bcif-static': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                options: DownloadStructurePdbIdSourceOptions
+            }, { isFlat: true }),
+            'url': PD.Group({
+                url: PD.Text(''),
+                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
+                isBinary: PD.Boolean(false),
+                options: PD.Group({
+                    supportProps: PD.Optional(PD.Boolean(false))
+                })
+            }, { isFlat: true })
+        }, {
+                options: [
+                    ['pdbe-updated', 'PDBe Updated'],
+                    ['rcsb', 'RCSB'],
+                    ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
+                    ['url', 'URL']
+                ]
+            })
+    }
+})(({ params, state }, ctx: PluginContext) => {
+    const b = state.build();
+    const src = params.source;
+    let downloadParams: StateTransformer.Params<Download>[];
+    let supportProps = false, asTrajectory = false;
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary }];
+            supportProps = !!src.params.options.supportProps;
+            break;
+        case 'pdbe-updated':
+            downloadParams = getDownloadParams(src.params.id, id => `https://www.ebi.ac.uk/pdbe/static/entry/${id.toLowerCase()}_updated.cif`, id => `PDBe: ${id}`, false);
+            supportProps = !!src.params.options.supportProps;
+            asTrajectory = !!src.params.options.asTrajectory;
+            break;
+        case 'rcsb':
+            downloadParams = getDownloadParams(src.params.id, id => `https://files.rcsb.org/download/${id.toUpperCase()}.cif`, id => `RCSB: ${id}`, false);
+            supportProps = !!src.params.options.supportProps;
+            asTrajectory = !!src.params.options.asTrajectory;
+            break;
+        case 'bcif-static':
+            downloadParams = getDownloadParams(src.params.id, id => `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${id.toLowerCase()}`, id => `BinaryCIF: ${id}`, true);
+            supportProps = !!src.params.options.supportProps;
+            asTrajectory = !!src.params.options.asTrajectory;
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    if (downloadParams.length > 0 && asTrajectory) {
+        const traj = createSingleTrajectoryModel(downloadParams, b);
+        createStructureTree(ctx, traj, supportProps);
+    } else {
+        for (const download of downloadParams) {
+            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } });
+            const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
+            createStructureTree(ctx, traj, supportProps)
+        }
+    }
+    return state.updateTree(b);
+});
+
+function getDownloadParams(src: string, url: (id: string) => string, label: (id: string) => string, isBinary: boolean): StateTransformer.Params<Download>[] {
+    const ids = src.split(',').map(id => id.trim()).filter(id => !!id && id.length >= 4);
+    const ret: StateTransformer.Params<Download>[] = [];
+    for (const id of ids) {
+        ret.push({ url: url(id), isBinary, label: label(id) })
+    }
+    return ret;
+}
+
+function createSingleTrajectoryModel(sources: StateTransformer.Params<Download>[], b: StateBuilder.Root) {
+    return b.toRoot()
+        .apply(StateTransforms.Data.DownloadBlob, {
+            sources: sources.map((src, i) => ({ id: '' + i, url: src.url, isBinary: src.isBinary })),
+            maxConcurrency: 6
+        }).apply(StateTransforms.Data.ParseBlob, {
+            formats: sources.map((_, i) => ({ id: '' + i, format: 'cif' as 'cif' }))
+        })
+        .apply(StateTransforms.Model.TrajectoryFromBlob)
+        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+}
+
+export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' | 'gro' = 'cif') {
+    let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory>
+    switch (format) {
+        case 'cif':
+            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } })
+                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } })
+            break
+        case 'pdb':
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } });
+            break
+        case 'gro':
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } });
+            break
+        default:
+            throw new Error('unsupported format')
+    }
+
+    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+}
+
+function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean) {
+    let root = b;
+    if (supportProps) {
+        root = root.apply(StateTransforms.Model.CustomModelProperties);
+    }
+    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
+    complexRepresentation(ctx, structure);
+
+    return root;
+}
+
+export function complexRepresentation(
+    ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>,
+    params?: { hideSequence?: boolean, hideHET?: boolean, hideWater?: boolean, hideCoarse?: boolean; }
+) {
+    if (!params || !params.hideSequence) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
+    }
+    if (!params || !params.hideHET) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
+    }
+    if (!params || !params.hideWater) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
+    }
+    if (!params || !params.hideCoarse) {
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+    }
+}
+
+export const CreateComplexRepresentation = StateAction.build({
+    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
+    from: PluginStateObject.Molecule.Structure
+})(({ ref, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref);
+    complexRepresentation(ctx, root);
+    return state.updateTree(root);
+});
+
+export const UpdateTrajectory = StateAction.build({
+    display: { name: 'Update Trajectory' },
+    params: {
+        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
+        by: PD.Optional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
+    }
+})(({ params, state }) => {
+    const models = state.selectQ(q => q.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
+
+    const update = state.build();
+
+    if (params.action === 'reset') {
+        for (const m of models) {
+            update.to(m).update({ modelIndex: 0 });
+        }
+    } else {
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj;
+            update.to(m).update(old => {
+                let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                if (modelIndex < 0) modelIndex += traj.data.length;
+                return { modelIndex };
+            });
+        }
+    }
+
+    return state.updateTree(update);
+});
+
+export const EnableModelCustomProps = StateAction.build({
+    display: { name: 'Custom Properties', description: 'Enable the addition of custom properties to the model.' },
+    from: PluginStateObject.Molecule.Model,
+    params(a, ctx: PluginContext) {
+        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
+        return { properties: ctx.customModelProperties.getSelect(a.data) };
+    },
+    isApplicable(a, t, ctx: PluginContext) {
+        return t.transformer !== CustomModelProperties;
+    }
+})(({ ref, params, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref).insert(CustomModelProperties, params);
+    return state.updateTree(root);
+});
+
+export const StructureFromSelection = StateAction.build({
+    display: { name: 'Selection Structure', description: 'Create a new Structure from the current selection.' },
+    from: PluginStateObject.Molecule.Structure,
+    params: {
+        label: PD.Text()
+    }
+    // isApplicable(a, t, ctx: PluginContext) {
+    //     return t.transformer !== CustomModelProperties;
+    // }
+})(({ a, ref, params, state }, plugin: PluginContext) => {
+    const sel = plugin.helpers.structureSelection.get(a.data);
+    if (sel.kind === 'empty-loci') return Task.constant('', void 0);
+
+    const query = StructureElement.Loci.toScriptExpression(sel);
+    const root = state.build().to(ref).apply(StructureSelection, { query, label: params.label });
+    return state.updateTree(root);
+});
\ No newline at end of file
diff --git a/src/mol-plugin/state/actions/volume.ts b/src/mol-plugin/state/actions/volume.ts
new file mode 100644
index 0000000000000000000000000000000000000000..727114bf9f8b56ec9b7c619130b28e208d79ab06
--- /dev/null
+++ b/src/mol-plugin/state/actions/volume.ts
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2018-2019 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 { VolumeIsoValue } from 'mol-model/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateAction, StateBuilder, StateTransformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { ColorNames } from 'mol-util/color/tables';
+import { FileInfo, getFileInfo } from 'mol-util/file-info';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { Download } from '../transforms/data';
+import { VolumeRepresentation3DHelpers } from '../transforms/representation';
+import { DataFormatProvider, guessCifVariant } from './data-format';
+
+export const Ccp4Provider: DataFormatProvider<any> = {
+    label: 'CCP4/MRC/BRIX',
+    description: 'CCP4/MRC/BRIX',
+    stringExtensions: [],
+    binaryExtensions: ['ccp4', 'mrc', 'map'],
+    isApplicable: (info: FileInfo, data: Uint8Array) => {
+        return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+        return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParseCcp4)
+                .apply(StateTransforms.Volume.VolumeFromCcp4)
+                .apply(StateTransforms.Representation.VolumeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
+
+export const Dsn6Provider: DataFormatProvider<any> = {
+    label: 'DSN6/BRIX',
+    description: 'DSN6/BRIX',
+    stringExtensions: [],
+    binaryExtensions: ['dsn6', 'brix'],
+    isApplicable: (info: FileInfo, data: Uint8Array) => {
+        return info.ext === 'dsn6' || info.ext === 'brix'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+        return Task.create('DSN6/BRIX default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParseDsn6)
+                .apply(StateTransforms.Volume.VolumeFromDsn6)
+                .apply(StateTransforms.Representation.VolumeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
+
+export const DscifProvider: DataFormatProvider<any> = {
+    label: 'DensityServer CIF',
+    description: 'DensityServer CIF',
+    stringExtensions: ['cif'],
+    binaryExtensions: ['bcif'],
+    isApplicable: (info: FileInfo, data: Uint8Array | string) => {
+        return guessCifVariant(info, data) === 'dscif' ? true : false
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, state: State) => {
+        return Task.create('DensityServer CIF default builder', async taskCtx => {
+            const cifBuilder = data.apply(StateTransforms.Data.ParseCif)
+            const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx)
+            const b = state.build().to(cifBuilder.ref);
+            const blocks = cifStateObject.data.blocks.slice(1); // zero block contains query meta-data
+            let tree: StateBuilder.To<any>
+            if (blocks.length === 1) {
+                tree = b
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
+            } else if (blocks.length === 2) {
+                tree = b
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue }))
+                const vol = tree.to(cifBuilder.ref)
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
+                const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
+                tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams)
+                const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })
+                tree = tree.to(vol.ref).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams)
+            } else {
+                throw new Error('unknown number of blocks')
+            }
+
+            await state.updateTree(tree).runInContext(taskCtx);
+        })
+    }
+}
+
+export { DownloadDensity };
+type DownloadDensity = typeof DownloadDensity
+const DownloadDensity = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
+    params: (a, ctx: PluginContext) => {
+        const { options } = ctx.dataFormat.registry
+        return {
+            source: PD.MappedStatic('rcsb', {
+                'pdbe': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
+                }, { isFlat: true }),
+                'pdbe-emd-ds': PD.Group({
+                    id: PD.Text('emd-8004', { label: 'Id' }),
+                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                }, { isFlat: true }),
+                'pdbe-xray-ds': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                }, { isFlat: true }),
+                'rcsb': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
+                }, { isFlat: true }),
+                'url': PD.Group({
+                    url: PD.Text(''),
+                    isBinary: PD.Boolean(false),
+                    format: PD.Select('auto', options),
+                }, { isFlat: true })
+            }, {
+                options: [
+                    ['pdbe', 'PDBe X-ray maps'],
+                    ['pdbe-emd-ds', 'PDBe EMD Density Server'],
+                    ['pdbe-xray-ds', 'PDBe X-ray Density Server'],
+                    ['rcsb', 'RCSB X-ray maps'],
+                    ['url', 'URL']
+                ]
+            })
+        }
+    }
+})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => {
+    const src = params.source;
+    let downloadParams: StateTransformer.Params<Download>;
+    let provider: DataFormatProvider<any>
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = src.params;
+            break;
+        case 'pdbe':
+            downloadParams = {
+                url: src.params.type === '2fofc'
+                    ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4`
+                    : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`,
+                isBinary: true,
+                label: `PDBe X-ray map: ${src.params.id}`
+            };
+            break;
+        case 'pdbe-emd-ds':
+            downloadParams = {
+                url: `https://www.ebi.ac.uk/pdbe/densities/emd/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
+                isBinary: true,
+                label: `PDBe EMD Density Server: ${src.params.id}`
+            };
+            break;
+        case 'pdbe-xray-ds':
+            downloadParams = {
+                url: `https://www.ebi.ac.uk/pdbe/densities/x-ray/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
+                isBinary: true,
+                label: `PDBe X-ray Density Server: ${src.params.id}`
+            };
+            break;
+        case 'rcsb':
+            downloadParams = {
+                url: src.params.type === '2fofc'
+                    ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6`
+                    : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`,
+                isBinary: true,
+                label: `RCSB X-ray map: ${src.params.id}`
+            };
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams);
+    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = src.params;
+            provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format)
+            break;
+        case 'pdbe':
+            provider = ctx.dataFormat.registry.get('ccp4')
+            break;
+        case 'pdbe-emd-ds':
+        case 'pdbe-xray-ds':
+            provider = ctx.dataFormat.registry.get('dscif')
+            break;
+        case 'rcsb':
+            provider = ctx.dataFormat.registry.get('dsn6')
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const b = state.build().to(data.ref);
+    await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
+}));
\ No newline at end of file
diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts
new file mode 100644
index 0000000000000000000000000000000000000000..308bc1bd6d07f909c3977287cf1a825719cc4b2f
--- /dev/null
+++ b/src/mol-plugin/state/animation/built-in.ts
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateAnimation } from './model';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { StateSelection, StateTransform } from 'mol-state';
+import { PluginCommands } from 'mol-plugin/command';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginContext } from 'mol-plugin/context';
+
+export const AnimateModelIndex = PluginStateAnimation.create({
+    name: 'built-in.animate-model-index',
+    display: { name: 'Animate Trajectory' },
+    params: () => ({
+        mode: PD.MappedStatic('palindrome', {
+            palindrome: PD.Group({ }),
+            loop: PD.Group({ }),
+            once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true })
+        }, { options: [['palindrome', 'Palindrome'], ['loop', 'Loop'], ['once', 'Once']] }),
+        maxFPS: PD.Numeric(15, { min: 1, max: 30, step: 1 })
+    }),
+    initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }),
+    async apply(animState, t, ctx) {
+        // limit fps
+        if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) {
+            return { kind: 'skip' };
+        }
+
+        const state = ctx.plugin.state.dataState;
+        const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
+
+        if (models.length === 0) {
+            // nothing more to do here
+            return { kind: 'finished' };
+        }
+
+        const update = state.build();
+
+        const params = ctx.params;
+        const palindromeDirections = animState.palindromeDirections || { };
+        let isEnd = false, allSingles = true;
+
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj;
+            update.to(m).update(old => {
+                const len = traj.data.length;
+                if (len !== 1) {
+                    allSingles = false;
+                } else {
+                    return old;
+                }
+                let dir: -1 | 1 = 1;
+                if (params.mode.name === 'once') {
+                    dir = params.mode.params.direction === 'backward' ? -1 : 1;
+                    // if we are at start or end already, do nothing.
+                    if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
+                        isEnd = true;
+                        return old;
+                    }
+                } else if (params.mode.name === 'palindrome') {
+                    if (old.modelIndex === 0) dir = 1;
+                    else if (old.modelIndex === len - 1) dir = -1;
+                    else dir = palindromeDirections[m.transform.ref] || 1;
+                }
+                palindromeDirections[m.transform.ref] = dir;
+
+                let modelIndex = (old.modelIndex + dir) % len;
+                if (modelIndex < 0) modelIndex += len;
+
+                isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
+
+                return { modelIndex };
+            });
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+
+        if (allSingles || (params.mode.name === 'once' && isEnd)) return { kind: 'finished' };
+        if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } };
+        return { kind: 'next', state: {} };
+    }
+})
+
+export const AnimateAssemblyUnwind = PluginStateAnimation.create({
+    name: 'built-in.animate-assembly-unwind',
+    display: { name: 'Unwind Assembly' },
+    params: (plugin: PluginContext) => {
+        const targets: [string, string][] = [['all', 'All']];
+        const structures = plugin.state.dataState.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure));
+
+        for (const s of structures) {
+            targets.push([s.transform.ref, s.obj!.data.models[0].label]);
+        }
+
+        return {
+            durationInMs: PD.Numeric(3000, { min: 100, max: 10000, step: 100}),
+            playOnce: PD.Boolean(false),
+            target: PD.Select(targets[0][0], targets)
+        };
+    },
+    initialState: () => ({ t: 0 }),
+    async setup(params, plugin) {
+        const state = plugin.state.dataState;
+        const root = !params.target || params.target === 'all' ? StateTransform.RootRef : params.target;
+        const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D, root));
+
+        const update = state.build();
+        let changed = false;
+        for (const r of reprs) {
+            // TODO: find a better way to handle this, perhaps add a different state object??
+            if (r.transform.transformer === StateTransforms.Representation.StructureLabels3D) continue;
+
+            const unwinds = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, r.transform.ref));
+            if (unwinds.length > 0) continue;
+
+            changed = true;
+            update.to(r)
+                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } });
+        }
+
+        if (!changed) return;
+
+        return plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
+    },
+    async teardown(_, plugin) {
+        const state = plugin.state.dataState;
+        const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
+            .filter(c => c.transform.props.tag === 'animate-assembly-unwind'));
+        if (reprs.length === 0) return;
+
+        const update = state.build();
+        for (const r of reprs) update.delete(r.transform.ref);
+        return plugin.runTask(state.updateTree(update));
+    },
+    async apply(animState, t, ctx) {
+        const state = ctx.plugin.state.dataState;
+        const root = !ctx.params.target || ctx.params.target === 'all' ? StateTransform.RootRef : ctx.params.target;
+        const anims = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, root));
+
+        if (anims.length === 0) {
+            return { kind: 'finished' };
+        }
+
+        const update = state.build();
+
+        const d = (t.current - t.lastApplied) / ctx.params.durationInMs;
+        let newTime = (animState.t + d), finished = false;
+        if (ctx.params.playOnce && newTime >= 1) {
+            finished = true;
+            newTime = 1;
+        } else {
+            newTime = newTime % 1;
+        }
+
+        for (const m of anims) {
+            update.to(m).update({ t: newTime });
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+
+        if (finished) return { kind: 'finished' };
+        return { kind: 'next', state: { t: newTime } };
+    }
+})
+
+export const AnimateUnitsExplode = PluginStateAnimation.create({
+    name: 'built-in.animate-units-explode',
+    display: { name: 'Explode Units' },
+    params: () => ({
+        durationInMs: PD.Numeric(3000, { min: 100, max: 10000, step: 100})
+    }),
+    initialState: () => ({ t: 0 }),
+    async setup(_, plugin) {
+        const state = plugin.state.dataState;
+        const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3D));
+
+        const update = state.build();
+        let changed = false;
+        for (const r of reprs) {
+            // TODO: find a better way to handle this, perhaps add a different state object??
+            if (r.transform.transformer === StateTransforms.Representation.StructureLabels3D) continue;
+
+            const explodes = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.ExplodeStructureRepresentation3D, r.transform.ref));
+            if (explodes.length > 0) continue;
+
+            changed = true;
+            update.to(r.transform.ref)
+                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { props: { tag: 'animate-units-explode' } });
+        }
+
+        if (!changed) return;
+
+        return plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
+    },
+    async teardown(_, plugin) {
+        const state = plugin.state.dataState;
+        const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
+            .filter(c => c.transform.props.tag === 'animate-units-explode'));
+        if (reprs.length === 0) return;
+
+        const update = state.build();
+        for (const r of reprs) update.delete(r.transform.ref);
+        return plugin.runTask(state.updateTree(update));
+    },
+    async apply(animState, t, ctx) {
+        const state = ctx.plugin.state.dataState;
+        const anims = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.ExplodeStructureRepresentation3D));
+
+        if (anims.length === 0) {
+            return { kind: 'finished' };
+        }
+
+        const update = state.build();
+
+        const d = (t.current - t.lastApplied) / ctx.params.durationInMs;
+        const newTime = (animState.t + d) % 1;
+
+        for (const m of anims) {
+            update.to(m).update({ t: newTime });
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+
+        return { kind: 'next', state: { t: newTime } };
+    }
+})
\ No newline at end of file
diff --git a/src/mol-plugin/state/animation/helpers.ts b/src/mol-plugin/state/animation/helpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..df6bfb962bbc35bfe78088aed47fc39df1d854f2
--- /dev/null
+++ b/src/mol-plugin/state/animation/helpers.ts
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+
+import { SymmetryOperator } from 'mol-math/geometry';
+import { Mat4, Vec3 } from 'mol-math/linear-algebra';
+import { Structure, StructureSelection, QueryContext } from 'mol-model/structure';
+import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+import { Color } from 'mol-util/color';
+import { Overpaint } from 'mol-theme/overpaint';
+import { parseMolScript } from 'mol-script/language/parser';
+import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
+import { compile } from 'mol-script/runtime/query/compiler';
+
+const _unwindMatrix = Mat4.zero();
+export function unwindStructureAssembly(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) {
+    for (let i = 0, _i = structure.units.length; i < _i; i++) {
+        const u = structure.units[i];
+        SymmetryOperator.lerpFromIdentity(_unwindMatrix, u.conformation.operator, t);
+        unitTransforms.setTransform(_unwindMatrix, u);
+    }
+}
+
+const _centerVec = Vec3.zero(), _transVec = Vec3.zero(), _transMat = Mat4.zero();
+export function explodeStructure(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) {
+    const boundary = structure.boundary.sphere;
+    const d = boundary.radius * t;
+
+    for (let i = 0, _i = structure.units.length; i < _i; i++) {
+        const u = structure.units[i];
+        Vec3.transformMat4(_centerVec, u.lookup3d.boundary.sphere.center, u.conformation.operator.matrix);
+        Vec3.sub(_transVec, _centerVec, boundary.center);
+        Vec3.setMagnitude(_transVec, _transVec, d);
+        Mat4.fromTranslation(_transMat, _transVec);
+
+        unitTransforms.setTransform(_transMat, u);
+    }
+}
+
+type ScriptLayers = { script: { language: string, expression: string }, color: Color }[]
+export function getStructureOverpaint(structure: Structure, scriptLayers: ScriptLayers, alpha: number): Overpaint {
+    const layers: Overpaint.Layer[] = []
+    for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+        const { script, color } = scriptLayers[i]
+        const parsed = parseMolScript(script.expression)
+        if (parsed.length === 0) throw new Error('No query')
+        const query = transpileMolScript(parsed[0])
+
+        const compiled = compile<StructureSelection>(query)
+        const result = compiled(new QueryContext(structure))
+        const loci = StructureSelection.toLoci2(result)
+
+        layers.push({ loci, color })
+    }
+    return { layers, alpha }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/animation/manager.ts b/src/mol-plugin/state/animation/manager.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2fe7350f341fe0c02387594b46dfe5b8aecf13dc
--- /dev/null
+++ b/src/mol-plugin/state/animation/manager.ts
@@ -0,0 +1,219 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginComponent } from 'mol-plugin/component';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateAnimation } from './model';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+export { PluginAnimationManager }
+
+// TODO: pause functionality (this needs to reset if the state tree changes)
+// TODO: handle unregistered animations on state restore
+// TODO: better API
+
+class PluginAnimationManager extends PluginComponent<PluginAnimationManager.State> {
+    private map = new Map<string, PluginStateAnimation>();
+    private animations: PluginStateAnimation[] = [];
+    private _current: PluginAnimationManager.Current;
+    private _params?: PD.For<PluginAnimationManager.State['params']> = void 0;
+
+    readonly events = {
+        updated: this.ev()
+    };
+
+    get isEmpty() { return this.animations.length === 0; }
+    get current() { return this._current!; }
+
+    private triggerUpdate() {
+        this.events.updated.next();
+    }
+
+    getParams(): PD.Params {
+        if (!this._params) {
+            this._params = {
+                current: PD.Select(this.animations[0] && this.animations[0].name,
+                    this.animations.map(a => [a.name, a.display.name] as [string, string]),
+                    { label: 'Animation' })
+            };
+        }
+        return this._params as any as PD.Params;
+    }
+
+    updateParams(newParams: Partial<PluginAnimationManager.State['params']>) {
+        this.updateState({ params: { ...this.state.params, ...newParams } });
+        const anim = this.map.get(this.state.params.current)!;
+        const params = anim.params(this.context) as PD.Params;
+        this._current = {
+            anim,
+            params,
+            paramValues: PD.getDefaultValues(params),
+            state: {},
+            startedTime: -1,
+            lastTime: 0
+        }
+        this.triggerUpdate();
+    }
+
+    updateCurrentParams(values: any) {
+        this._current.paramValues = { ...this._current.paramValues, ...values };
+        this.triggerUpdate();
+    }
+
+    register(animation: PluginStateAnimation) {
+        if (this.map.has(animation.name)) {
+            this.context.log.error(`Animation '${animation.name}' is already registered.`);
+            return;
+        }
+        this._params = void 0;
+        this.map.set(animation.name, animation);
+        this.animations.push(animation);
+        if (this.animations.length === 1) {
+            this.updateParams({ current: animation.name });
+        } else {
+            this.triggerUpdate();
+        }
+    }
+
+    play<P>(animation: PluginStateAnimation<P>, params: P) {
+        this.stop();
+        if (!this.map.has(animation.name)) {
+            this.register(animation);
+        }
+        this.updateParams({ current: animation.name });
+        this.updateCurrentParams(params);
+        this.start();
+    }
+
+    async start() {
+        this.updateState({ animationState: 'playing' });
+        if (!this.context.behaviors.state.isAnimating.value) {
+            this.context.behaviors.state.isAnimating.next(true);
+        }
+        this.triggerUpdate();
+
+        const anim = this._current.anim;
+        if (anim.setup) {
+            await anim.setup(this._current.paramValues, this.context);
+        }
+
+        this._current.lastTime = 0;
+        this._current.startedTime = -1;
+        this._current.state = this._current.anim.initialState(anim, this.context);
+
+        requestAnimationFrame(this.animate);
+    }
+
+    async stop() {
+        if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame);
+
+        if (this.state.animationState !== 'stopped') {
+            const anim = this._current.anim;
+            if (anim.teardown) {
+                await anim.teardown(this._current.paramValues, this.context);
+            }
+
+            this.updateState({ animationState: 'stopped' });
+            this.triggerUpdate();
+        }
+
+        if (this.context.behaviors.state.isAnimating.value) {
+            this.context.behaviors.state.isAnimating.next(false);
+        }
+    }
+
+    get isAnimating() {
+        return this.state.animationState === 'playing';
+    }
+
+    private _frame: number | undefined = void 0;
+    private animate = async (t: number) => {
+        this._frame = void 0;
+
+        if (this._current.startedTime < 0) this._current.startedTime = t;
+        const newState = await this._current.anim.apply(
+            this._current.state,
+            { lastApplied: this._current.lastTime, current: t - this._current.startedTime },
+            { params: this._current.paramValues, plugin: this.context });
+
+        if (newState.kind === 'finished') {
+            this.stop();
+        } else if (newState.kind === 'next') {
+            this._current.state = newState.state;
+            this._current.lastTime = t - this._current.startedTime;
+            if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
+        } else if (newState.kind === 'skip') {
+            if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
+        }
+    }
+
+    getSnapshot(): PluginAnimationManager.Snapshot {
+        if (!this.current) return { state: this.state };
+
+        return {
+            state: this.state,
+            current: {
+                paramValues: this._current.paramValues,
+                state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state
+            }
+        };
+    }
+
+    setSnapshot(snapshot: PluginAnimationManager.Snapshot) {
+        this.updateState({ animationState: snapshot.state.animationState });
+        this.updateParams(snapshot.state.params);
+
+        if (snapshot.current) {
+            this.current.paramValues = snapshot.current.paramValues;
+            this.current.state = this._current.anim.stateSerialization
+                ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state)
+                : snapshot.current.state;
+            this.triggerUpdate();
+            if (this.state.animationState === 'playing') this.resume();
+        }
+    }
+
+    private async resume() {
+        this._current.lastTime = 0;
+        this._current.startedTime = -1;
+        const anim = this._current.anim;
+        if (!this.context.behaviors.state.isAnimating.value) {
+            this.context.behaviors.state.isAnimating.next(true);
+        }
+        if (anim.setup) {
+            await anim.setup(this._current.paramValues, this.context);
+        }
+        requestAnimationFrame(this.animate);
+    }
+
+    constructor(private context: PluginContext) {
+        super({ params: { current: '' }, animationState: 'stopped' });
+    }
+}
+
+namespace PluginAnimationManager {
+    export interface Current {
+        anim: PluginStateAnimation
+        params: PD.Params,
+        paramValues: any,
+        state: any,
+        startedTime: number,
+        lastTime: number
+    }
+
+    export interface State {
+        params: { current: string },
+        animationState: 'stopped' | 'playing'
+    }
+
+    export interface Snapshot {
+        state: State,
+        current?: {
+            paramValues: any,
+            state: any
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/animation/model.ts b/src/mol-plugin/state/animation/model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5759feca7b0ced9e1975f6e9d4dc1c1620475f9d
--- /dev/null
+++ b/src/mol-plugin/state/animation/model.ts
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2019 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';
+import { PluginContext } from 'mol-plugin/context';
+
+export { PluginStateAnimation }
+
+// TODO: helpers for building animations (once more animations are added)
+//       for example "composite animation"
+
+interface PluginStateAnimation<P = any, S = any> {
+    name: string,
+    readonly display: { readonly name: string, readonly description?: string },
+    params(ctx: PluginContext): PD.For<P>,
+    initialState(params: P, ctx: PluginContext): S,
+
+    // TODO: support state in setup/teardown?
+    setup?(params: P, ctx: PluginContext): void | Promise<void>,
+    teardown?(params: P, ctx: PluginContext): void | Promise<void>,
+
+    /**
+     * Apply the current frame and modify the state.
+     * @param t Current absolute time since the animation started.
+     */
+    apply(state: S, t: PluginStateAnimation.Time, ctx: PluginStateAnimation.Context<P>): Promise<PluginStateAnimation.ApplyResult<S>>,
+
+    /**
+     * The state must be serializable to JSON. If JSON.stringify is not enough,
+     * custom converted to an object that works with JSON.stringify can be provided.
+     */
+    stateSerialization?: { toJSON(state: S): any, fromJSON(data: any): S }
+}
+
+namespace PluginStateAnimation {
+    export interface Time {
+        lastApplied: number,
+        current: number
+    }
+
+    export type ApplyResult<S> = { kind: 'finished' } | { kind: 'skip' } | { kind: 'next', state: S }
+    export interface Context<P> {
+        params: P,
+        plugin: PluginContext
+    }
+
+    export function create<P, S>(params: PluginStateAnimation<P, S>) {
+        return params;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/state/camera.ts b/src/mol-plugin/state/camera.ts
index b05a2b0692879bd8a0636b44f4ab53d362e2eae3..830dbaf8de461f343944f16c1d1e1cfc21021579 100644
--- a/src/mol-plugin/state/camera.ts
+++ b/src/mol-plugin/state/camera.ts
@@ -7,57 +7,53 @@
 import { Camera } from 'mol-canvas3d/camera';
 import { OrderedMap } from 'immutable';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
+import { PluginComponent } from 'mol-plugin/component';
 
 export { CameraSnapshotManager }
 
-class CameraSnapshotManager {
-    private ev = RxEventHelper.create();
-    private _entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
-
+class CameraSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, CameraSnapshotManager.Entry> }> {
     readonly events = {
         changed: this.ev()
     };
 
-    get entries() { return this._entries; }
-
     getEntry(id: string) {
-        return this._entries.get(id);
+        return this.state.entries.get(id);
     }
 
     remove(id: string) {
-        if (!this._entries.has(id)) return;
-        this._entries.delete(id);
+        if (!this.state.entries.has(id)) return;
+        this.updateState({ entries: this.state.entries.delete(id) });
         this.events.changed.next();
     }
 
     add(e: CameraSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.updateState({ entries: this.state.entries.set(e.id, e) });
         this.events.changed.next();
     }
 
     clear() {
-        if (this._entries.size === 0) return;
-        this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
+        if (this.state.entries.size === 0) return;
+        this.updateState({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() });
         this.events.changed.next();
     }
 
     getStateSnapshot(): CameraSnapshotManager.StateSnapshot {
         const entries: CameraSnapshotManager.Entry[] = [];
-        this._entries.forEach(e => entries.push(e!));
+        this.state.entries.forEach(e => entries.push(e!));
         return { entries };
     }
 
     setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) {
-        this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
+        const entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
         for (const e of state.entries) {
-            this._entries.set(e.id, e);
+            entries.set(e.id, e);
         }
+        this.updateState({ entries: entries.asImmutable() });
         this.events.changed.next();
     }
 
-    dispose() {
-        this.ev.dispose();
+    constructor() {
+        super({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() });
     }
 }
 
diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts
index 1ff541cc0613fe20841191b2a4dd75cc1aaba683..0664a86050294048dffccdf474e34e5ad2a33749 100644
--- a/src/mol-plugin/state/objects.ts
+++ b/src/mol-plugin/state/objects.ts
@@ -1,7 +1,8 @@
 /**
- * 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>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { CifFile } from 'mol-io/reader/cif';
@@ -10,9 +11,11 @@ import { Model as _Model, Structure as _Structure } from 'mol-model/structure';
 import { VolumeData } from 'mol-model/volume';
 import { PluginBehavior } from 'mol-plugin/behavior/behavior';
 import { Representation } from 'mol-repr/representation';
-import { StructureRepresentation } from 'mol-repr/structure/representation';
+import { StructureRepresentation, StructureRepresentationState } from 'mol-repr/structure/representation';
 import { VolumeRepresentation } from 'mol-repr/volume/representation';
-import { StateObject, Transformer } from 'mol-state';
+import { StateObject, StateTransformer } from 'mol-state';
+import { Ccp4File } from 'mol-io/reader/ccp4/schema';
+import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
 import { Shape as _Shape } from 'mol-model/shape';
 import { ShapeProvider } from 'mol-model/shape/provider';
@@ -27,7 +30,7 @@ export namespace PluginStateObject {
 
     export const Create = StateObject.factory<TypeInfo>();
 
-    export function isRepresentation3D(o?: Any): o is StateObject<Representation.Any, TypeInfo> {
+    export function isRepresentation3D(o?: Any): o is StateObject<Representation3DData<Representation.Any>, TypeInfo> {
         return !!o && o.type.typeClass === 'Representation3D';
     }
 
@@ -35,8 +38,9 @@ export namespace PluginStateObject {
         return !!o && o.type.typeClass === 'Behavior';
     }
 
-    export function CreateRepresentation3D<T extends Representation.Any>(type: { name: string }) {
-        return Create<T>({ ...type, typeClass: 'Representation3D' })
+    export interface Representation3DData<T extends Representation.Any, S extends StateObject = StateObject> { repr: T, source: S }
+    export function CreateRepresentation3D<T extends Representation.Any, S extends StateObject = StateObject>(type: { name: string }) {
+        return Create<Representation3DData<T, S>>({ ...type, typeClass: 'Representation3D' });
     }
 
     export function CreateBehavior<T extends PluginBehavior>(type: { name: string }) {
@@ -44,30 +48,57 @@ export namespace PluginStateObject {
     }
 
     export class Root extends Create({ name: 'Root', typeClass: 'Root' }) { }
-
     export class Group extends Create({ name: 'Group', typeClass: 'Group' }) { }
 
     export namespace Data {
         export class String extends Create<string>({ name: 'String Data', typeClass: 'Data', }) { }
         export class Binary extends Create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data' }) { }
 
-        // TODO
-        // export class MultipleRaw extends Create<{
-        //     [key: string]: { type: 'String' | 'Binary', data: string | Uint8Array }
-        // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { }
+        export type BlobEntry = { id: string } &
+            ( { kind: 'string', data: string }
+            | { kind: 'binary', data: Uint8Array })
+        export type BlobData = BlobEntry[]
+        export class Blob extends Create<BlobData>({ name: 'Data Blob', typeClass: 'Data' }) { }
     }
 
     export namespace Format {
         export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
         export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
         export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { }
+        export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { }
+        export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { }
+
+        export type BlobEntry = { id: string } &
+            ( { kind: 'json', data: unknown }
+            | { kind: 'string', data: string }
+            | { kind: 'binary', data: Uint8Array }
+            | { kind: 'cif', data: CifFile }
+            | { kind: 'ccp4', data: Ccp4File }
+            | { kind: 'dsn6', data: Dsn6File }
+            | { kind: 'ply', data: PlyFile }
+            // For non-build in extensions
+            | { kind: 'custom', data: unknown, tag: string })
+        export type BlobData = BlobEntry[]
+        export class Blob extends Create<BlobData>({ name: 'Format Blob', typeClass: 'Data' }) { }
     }
 
     export namespace Molecule {
         export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { }
         export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { }
         export class Structure extends Create<_Structure>({ name: 'Structure', typeClass: 'Object' }) { }
-        export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any>>({ name: 'Structure 3D' }) { }
+
+        export namespace Structure {
+            export class Representation3D extends CreateRepresentation3D<StructureRepresentation<any> | ShapeRepresentation<any, any, any>, Structure>({ name: 'Structure 3D' }) { }
+
+            export interface Representation3DStateData {
+                source: Representation3D,
+                /** used to restore state when the obj is removed */
+                initialState: Partial<StructureRepresentationState>,
+                state: Partial<StructureRepresentationState>,
+                info?: unknown
+            }
+            export class Representation3DState extends Create<Representation3DStateData>({ name: 'Structure 3D State', typeClass: 'Object' }) { }
+        }
     }
 
     export namespace Volume {
@@ -82,6 +113,6 @@ export namespace PluginStateObject {
 }
 
 export namespace PluginStateTransform {
-    export const CreateBuiltIn = Transformer.factory('ms-plugin');
-    export const BuiltIn = Transformer.builderFactory('ms-plugin');
+    export const CreateBuiltIn = StateTransformer.factory('ms-plugin');
+    export const BuiltIn = StateTransformer.builderFactory('ms-plugin');
 }
\ No newline at end of file
diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts
index 8d03ed5ae64a12902d462b635d2826350e5eae55..318925085f7891c62039ec79bdaa114fbc66face 100644
--- a/src/mol-plugin/state/snapshots.ts
+++ b/src/mol-plugin/state/snapshots.ts
@@ -4,63 +4,248 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { OrderedMap } from 'immutable';
+import { List } from 'immutable';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { PluginState } from '../state';
+import { PluginComponent } from 'mol-plugin/component';
+import { PluginContext } from 'mol-plugin/context';
 
 export { PluginStateSnapshotManager }
 
-class PluginStateSnapshotManager {
-    private ev = RxEventHelper.create();
-    private _entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable();
+class PluginStateSnapshotManager extends PluginComponent<{
+    current?: UUID | undefined,
+    entries: List<PluginStateSnapshotManager.Entry>,
+    isPlaying: boolean,
+    nextSnapshotDelayInMs: number
+}> {
+    static DefaultNextSnapshotDelayInMs = 1500;
+
+    private entryMap = new Map<string, PluginStateSnapshotManager.Entry>();
 
     readonly events = {
         changed: this.ev()
     };
 
-    get entries() { return this._entries; }
+    currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;
+
+    getIndex(e: PluginStateSnapshotManager.Entry) {
+        return this.state.entries.indexOf(e);
+    }
 
-    getEntry(id: string) {
-        return this._entries.get(id);
+    getEntry(id: string | undefined) {
+        if (!id) return;
+        return this.entryMap.get(id);
     }
 
     remove(id: string) {
-        if (!this._entries.has(id)) return;
-        this._entries.delete(id);
+        const e = this.entryMap.get(id);
+        if (!e) return;
+
+        this.entryMap.delete(id);
+        this.updateState({
+            current: this.state.current === id ? void 0 : this.state.current,
+            entries: this.state.entries.delete(this.getIndex(e))
+        });
         this.events.changed.next();
     }
 
     add(e: PluginStateSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.entryMap.set(e.snapshot.id, e);
+        this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
+        this.events.changed.next();
+    }
+
+    replace(id: string, snapshot: PluginState.Snapshot) {
+        const old = this.getEntry(id);
+        if (!old) return;
+
+        const idx = this.getIndex(old);
+        // The id changes here!
+        const e = PluginStateSnapshotManager.Entry(snapshot, {
+            name: old.name,
+            description: old.description
+        });
+        this.entryMap.set(snapshot.id, e);
+        this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
+        this.events.changed.next();
+    }
+
+    move(id: string, dir: -1 | 1) {
+        const len = this.state.entries.size;
+        if (len < 2) return;
+
+        const e = this.getEntry(id);
+        if (!e) return;
+        const from = this.getIndex(e);
+        let to = (from + dir) % len;
+        if (to < 0) to += len;
+        const f = this.state.entries.get(to);
+
+        const entries = this.state.entries.asMutable();
+        entries.set(to, e);
+        entries.set(from, f);
+
+        this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
         this.events.changed.next();
     }
 
     clear() {
-        if (this._entries.size === 0) return;
-        this._entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable();
+        if (this.state.entries.size === 0) return;
+        this.entryMap.clear();
+        this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
+        this.events.changed.next();
+    }
+
+    setCurrent(id: string) {
+        const e = this.getEntry(id);
+        if (e) {
+            this.updateState({ current: id as UUID });
+            this.events.changed.next();
+        }
+        return e && e.snapshot;
+    }
+
+    getNextId(id: string | undefined, dir: -1 | 1) {
+        const len = this.state.entries.size;
+        if (!id) {
+            if (len === 0) return void 0;
+            const idx = dir === -1 ? len - 1 : 0;
+            return this.state.entries.get(idx).snapshot.id;
+        }
+
+        const e = this.getEntry(id);
+        if (!e) return;
+        let idx = this.getIndex(e);
+        if (idx < 0) return;
+
+        idx = (idx + dir) % len;
+        if (idx < 0) idx += len;
+
+        return this.state.entries.get(idx).snapshot.id;
+    }
+
+    async setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): Promise<PluginState.Snapshot | undefined> {
+        this.clear();
+        const entries = List<PluginStateSnapshotManager.Entry>().asMutable()
+        for (const e of snapshot.entries) {
+            this.entryMap.set(e.snapshot.id, e);
+            entries.push(e);
+        }
+        const current = snapshot.current
+            ? snapshot.current
+            : snapshot.entries.length > 0
+            ? snapshot.entries[0].snapshot.id
+            : void 0;
+        this.updateState({
+            current,
+            entries: entries.asImmutable(),
+            isPlaying: false,
+            nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
+        });
         this.events.changed.next();
+        if (!current) return;
+        const entry = this.getEntry(current);
+        const next = entry && entry.snapshot;
+        if (!next) return;
+        await this.plugin.state.setSnapshot(next);
+        if (snapshot.playback && snapshot.playback.isPlaying) this.play(true);
+        return next;
     }
 
-    dispose() {
-        this.ev.dispose();
+    getRemoteSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean }): PluginStateSnapshotManager.RemoteSnapshot {
+        // TODO: diffing and all that fancy stuff
+        return {
+            timestamp: +new Date(),
+            name: options && options.name,
+            description: options && options.description,
+            current: this.state.current,
+            playback: {
+                isPlaying: !!(options && options.playOnLoad),
+                nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
+            },
+            entries: this.state.entries.valueSeq().toArray()
+        };
+    }
+
+    private timeoutHandle: any = void 0;
+    private next = async () => {
+        this.timeoutHandle = void 0;
+        const next = this.getNextId(this.state.current, 1);
+        if (!next || next === this.state.current) {
+            this.stop();
+            return;
+        }
+        const snapshot = this.setCurrent(next)!;
+        await this.plugin.state.setSnapshot(snapshot);
+        const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
+        if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
+    };
+
+    play(delayFirst: boolean = false) {
+        this.updateState({ isPlaying: true });
+
+        if (delayFirst) {
+            const e = this.getEntry(this.state.current);
+            if (!e) {
+                this.next();
+                return;
+            }
+            this.events.changed.next();
+            const snapshot = e.snapshot;
+            const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
+            this.timeoutHandle = setTimeout(this.next, delay);
+        } else {
+            this.next();
+        }
+    }
+
+    stop() {
+        this.updateState({ isPlaying: false });
+        if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
+        this.timeoutHandle = void 0;
+        this.events.changed.next();
+    }
+
+    togglePlay() {
+        if (this.state.isPlaying) {
+            this.stop();
+            this.plugin.state.animation.stop();
+        }
+        else this.play();
+    }
+
+    constructor(private plugin: PluginContext) {
+        super({
+            current: void 0,
+            entries: List(),
+            isPlaying: false,
+            nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
+        });
+        // TODO make nextSnapshotDelayInMs editable
     }
 }
 
 namespace PluginStateSnapshotManager {
     export interface Entry {
-        id: UUID,
-        timestamp: string,
+        timestamp: number,
         name?: string,
         description?: string,
         snapshot: PluginState.Snapshot
     }
 
-    export function Entry(snapshot: PluginState.Snapshot, name?: string, description?: string): Entry {
-        return { id: UUID.create22(), timestamp: new Date().toLocaleString(), name, snapshot, description };
+    export function Entry(snapshot: PluginState.Snapshot, params: {name?: string, description?: string }): Entry {
+        return { timestamp: +new Date(), snapshot, ...params };
     }
 
-    export interface StateSnapshot {
+    export interface RemoteSnapshot {
+        timestamp: number,
+        name?: string,
+        description?: string,
+        current: UUID | undefined,
+        playback: {
+            isPlaying: boolean,
+            nextSnapshotDelayInMs: number,
+        },
         entries: Entry[]
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms.ts b/src/mol-plugin/state/transforms.ts
index c6914174cf71c4d92ec64916da5341ea78ff6b84..1058f7ee58a74546b4cbd0810bcac92034393d9f 100644
--- a/src/mol-plugin/state/transforms.ts
+++ b/src/mol-plugin/state/transforms.ts
@@ -5,11 +5,15 @@
  */
 
 import * as Data from './transforms/data'
+import * as Misc from './transforms/misc'
 import * as Model from './transforms/model'
+import * as Volume from './transforms/volume'
 import * as Representation from './transforms/representation'
 
 export const StateTransforms = {
     Data,
+    Misc,
     Model,
+    Volume,
     Representation
 }
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts
index 4f9e663a06c1979887baf909070bc5e219af1486..7481cb8e76f859808125cb079a92ae0bc9d74bc7 100644
--- a/src/mol-plugin/state/transforms/data.ts
+++ b/src/mol-plugin/state/transforms/data.ts
@@ -1,7 +1,8 @@
 /**
- * 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>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { PluginStateTransform } from '../objects';
@@ -10,8 +11,10 @@ import { Task } from 'mol-task';
 import CIF from 'mol-io/reader/cif'
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Transformer } from 'mol-state';
-import { readFromFile } from 'mol-util/data-source';
+import { StateTransformer } from 'mol-state';
+import { readFromFile, ajaxGetMany } from 'mol-util/data-source';
+import * as CCP4 from 'mol-io/reader/ccp4/parser'
+import * as DSN6 from 'mol-io/reader/dsn6/parser'
 import * as PLY from 'mol-io/reader/ply/parse_data/ply_parser'
 
 export { Download }
@@ -23,28 +26,74 @@ const Download = PluginStateTransform.BuiltIn({
     to: [SO.Data.String, SO.Data.Binary],
     params: {
         url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }),
-        label: PD.makeOptional(PD.Text('')),
-        isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' }))
+        label: PD.Optional(PD.Text('')),
+        isBinary: PD.Optional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' }))
     }
 })({
     apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
-            const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string').runInContext(ctx);
+            const data = await globalCtx.fetch({ url: p.url, type: p.isBinary ? 'binary' : 'string' }).runInContext(ctx);
             return p.isBinary
                 ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url })
                 : new SO.Data.String(data as string, { label: p.label ? p.label : p.url });
         });
     },
     update({ oldParams, newParams, b }) {
-        if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate;
+        if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate;
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || newParams.url;
-            return Transformer.UpdateResult.Updated;
+            return StateTransformer.UpdateResult.Updated;
         }
-        return Transformer.UpdateResult.Unchanged;
+        return StateTransformer.UpdateResult.Unchanged;
     }
 });
 
+export { DownloadBlob }
+type DownloadBlob = typeof DownloadBlob
+const DownloadBlob = PluginStateTransform.BuiltIn({
+    name: 'download-blob',
+    display: { name: 'Download Blob', description: 'Download multiple string or binary data from the specified URLs.' },
+    from: SO.Root,
+    to: SO.Data.Blob,
+    params: {
+        sources: PD.ObjectList({
+            id: PD.Text('', { label: 'Unique ID' }),
+            url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }),
+            isBinary: PD.Optional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' })),
+            canFail: PD.Optional(PD.Boolean(false, { description: 'Indicate whether the download can fail and not be included in the blob as a result.' }))
+        }, e => `${e.id}: ${e.url}`),
+        maxConcurrency: PD.Optional(PD.Numeric(4, { min: 1, max: 12, step: 1 }, { description: 'The maximum number of concurrent downloads.' }))
+    }
+})({
+    apply({ params }, plugin: PluginContext) {
+        return Task.create('Download Blob', async ctx => {
+            const entries: SO.Data.BlobEntry[] = [];
+            const data = await ajaxGetMany(ctx, params.sources, params.maxConcurrency || 4);
+
+            for (let i = 0; i < data.length; i++) {
+                const r = data[i], src = params.sources[i];
+                if (r.kind === 'error') plugin.log.warn(`Download ${r.id} (${src.url}) failed: ${r.error}`);
+                else {
+                    entries.push(src.isBinary
+                        ? { id: r.id, kind: 'binary', data: r.result as Uint8Array }
+                        : { id: r.id, kind: 'string', data: r.result as string });
+                }
+            }
+            return new SO.Data.Blob(entries, { label: 'Data Blob', description: `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}` });
+        });
+    },
+    // TODO: ??
+    // update({ oldParams, newParams, b }) {
+    //     return 0 as any;
+    //     // if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate;
+    //     // if (oldParams.label !== newParams.label) {
+    //     //     (b.label as string) = newParams.label || newParams.url;
+    //     //     return StateTransformer.UpdateResult.Updated;
+    //     // }
+    //     // return StateTransformer.UpdateResult.Unchanged;
+    // }
+});
+
 export { ReadFile }
 type ReadFile = typeof ReadFile
 const ReadFile = PluginStateTransform.BuiltIn({
@@ -54,8 +103,8 @@ const ReadFile = PluginStateTransform.BuiltIn({
     to: [SO.Data.String, SO.Data.Binary],
     params: {
         file: PD.File(),
-        label: PD.makeOptional(PD.Text('')),
-        isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
+        label: PD.Optional(PD.Text('')),
+        isBinary: PD.Optional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
     }
 })({
     apply({ params: p }) {
@@ -69,13 +118,57 @@ const ReadFile = PluginStateTransform.BuiltIn({
     update({ oldParams, newParams, b }) {
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || oldParams.file.name;
-            return Transformer.UpdateResult.Updated;
+            return StateTransformer.UpdateResult.Updated;
         }
-        return Transformer.UpdateResult.Unchanged;
+        return StateTransformer.UpdateResult.Unchanged;
     },
     isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' })
 });
 
+export { ParseBlob }
+type ParseBlob = typeof ParseBlob
+const ParseBlob = PluginStateTransform.BuiltIn({
+    name: 'parse-blob',
+    display: { name: 'Parse Blob', description: 'Parse multiple data enties' },
+    from: SO.Data.Blob,
+    to: SO.Format.Blob,
+    params: {
+        formats: PD.ObjectList({
+            id: PD.Text('', { label: 'Unique ID' }),
+            format: PD.Select<'cif'>('cif', [['cif', 'cif']])
+        }, e => `${e.id}: ${e.format}`)
+    }
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Parse Blob', async ctx => {
+            const map = new Map<string, string>();
+            for (const f of params.formats) map.set(f.id, f.format);
+
+            const entries: SO.Format.BlobEntry[] = [];
+
+            for (const e of a.data) {
+                if (!map.has(e.id)) continue;
+
+                const parsed = await (e.kind === 'string' ? CIF.parse(e.data) : CIF.parseBinary(e.data)).runInContext(ctx);
+                if (parsed.isError) throw new Error(`${e.id}: ${parsed.message}`);
+                entries.push({ id: e.id, kind: 'cif', data: parsed.result });
+            }
+
+            return new SO.Format.Blob(entries, { label: 'Format Blob', description: `${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}` });
+        });
+    },
+    // TODO: ??
+    // update({ oldParams, newParams, b }) {
+    //     return 0 as any;
+    //     // if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate;
+    //     // if (oldParams.label !== newParams.label) {
+    //     //     (b.label as string) = newParams.label || newParams.url;
+    //     //     return StateTransformer.UpdateResult.Updated;
+    //     // }
+    //     // return StateTransformer.UpdateResult.Unchanged;
+    // }
+});
+
 export { ParseCif }
 type ParseCif = typeof ParseCif
 const ParseCif = PluginStateTransform.BuiltIn({
@@ -108,4 +201,38 @@ const ParsePly = PluginStateTransform.BuiltIn({
             return new SO.Format.Ply(parsed.result, { label: parsed.result.name || 'PLY Data' });
         });
     }
+});
+
+export { ParseCcp4 }
+type ParseCcp4 = typeof ParseCcp4
+const ParseCcp4 = PluginStateTransform.BuiltIn({
+    name: 'parse-ccp4',
+    display: { name: 'Parse CCP4/MRC/MAP', description: 'Parse CCP4/MRC/MAP from Binary data' },
+    from: [SO.Data.Binary],
+    to: SO.Format.Ccp4
+})({
+    apply({ a }) {
+        return Task.create('Parse CCP4/MRC/MAP', async ctx => {
+            const parsed = await CCP4.parse(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Ccp4(parsed.result);
+        });
+    }
+});
+
+export { ParseDsn6 }
+type ParseDsn6 = typeof ParseDsn6
+const ParseDsn6 = PluginStateTransform.BuiltIn({
+    name: 'parse-dsn6',
+    display: { name: 'Parse DSN6/BRIX', description: 'Parse CCP4/BRIX from Binary data' },
+    from: [SO.Data.Binary],
+    to: SO.Format.Dsn6
+})({
+    apply({ a }) {
+        return Task.create('Parse DSN6/BRIX', async ctx => {
+            const parsed = await DSN6.parse(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            return new SO.Format.Dsn6(parsed.result);
+        });
+    }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/misc.ts b/src/mol-plugin/state/transforms/misc.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69aac3a93e405563d7c0a55c3b442439a25a5fff
--- /dev/null
+++ b/src/mol-plugin/state/transforms/misc.ts
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTransformer } from 'mol-state';
+import { shallowEqual } from 'mol-util';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+
+export { CreateGroup };
+type CreateGroup = typeof CreateGroup
+const CreateGroup = PluginStateTransform.BuiltIn({
+    name: 'create-group',
+    display: { name: 'Parse CIF', description: 'Parse CIF from String or Binary data' },
+    from: [],
+    to: SO.Group,
+    params: {
+        label: PD.Text('Group'),
+        description: PD.Optional(PD.Text(''))
+    }
+})({
+    apply({ params }) {
+        return new SO.Group({}, params);
+    },
+    update({ oldParams, newParams, b }) {
+        if (shallowEqual(oldParams, newParams)) return StateTransformer.UpdateResult.Unchanged;
+        b.label = newParams.label;
+        b.description = newParams.description;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts
index 54434b2e50d606df8caae1096df38cc08fdeda22..755ccf908678cbab9c6fccc533bd5bd9093d94e9 100644
--- a/src/mol-plugin/state/transforms/model.ts
+++ b/src/mol-plugin/state/transforms/model.ts
@@ -1,23 +1,68 @@
 /**
- * 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>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { PluginStateTransform } from '../objects';
-import { PluginStateObject as SO } from '../objects';
-import { Task, RuntimeContext } from 'mol-task';
-import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { parsePDB } from 'mol-io/reader/pdb/parser';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
+import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
+import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry, QueryFn } from 'mol-model/structure';
+import { Assembly } from 'mol-model/structure/model/properties/symmetry';
+import { PluginContext } from 'mol-plugin/context';
+import { MolScriptBuilder } from 'mol-script/language/builder';
 import Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
-import { MolScriptBuilder } from 'mol-script/language/builder';
-import { StateObject } from 'mol-state';
-import { PluginContext } from 'mol-plugin/context';
+import { StateObject, StateTransformer } from 'mol-state';
+import { RuntimeContext, Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { stringToWords } from 'mol-util/string';
 import { shapeFromPly } from 'mol-model/shape/formarts/ply/plyData_to_shape';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+import { trajectoryFromGRO } from 'mol-model-formats/structure/gro';
+import { parseGRO } from 'mol-io/reader/gro/parser';
+import { parseMolScript } from 'mol-script/language/parser';
+import { transpileMolScript } from 'mol-script/script/mol-script/symbols';
+
+export { TrajectoryFromBlob };
+export { TrajectoryFromMmCif };
+export { TrajectoryFromPDB };
+export { TrajectoryFromGRO };
+export { ModelFromTrajectory };
+export { StructureFromModel };
+export { StructureAssemblyFromModel };
+export { StructureSymmetryFromModel };
+export { StructureSelection };
+export { UserStructureSelection };
+export { StructureComplexElement };
+export { CustomModelProperties };
+
+type TrajectoryFromBlob = typeof TrajectoryFromBlob
+const TrajectoryFromBlob = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-blob',
+    display: { name: 'Parse Blob', description: 'Parse format blob into a single trajectory.' },
+    from: SO.Format.Blob,
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a }) {
+        return Task.create('Parse Format Blob', async ctx => {
+            const models: Model[] = [];
+            for (const e of a.data) {
+                if (e.kind !== 'cif') continue;
+                const block = e.data.blocks[0];
+                const xs = await trajectoryFromMmCIF(block).runInContext(ctx);
+                if (xs.length === 0) throw new Error('No models found.');
+                for (const x of xs) models.push(x);
+            }
+
+            const props = { label: `Trajectory`, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
 
-export { TrajectoryFromMmCif }
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
@@ -25,9 +70,14 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     from: SO.Format.Cif,
     to: SO.Molecule.Trajectory,
     params(a) {
+        if (!a) {
+            return {
+                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+            };
+        }
         const { blocks } = a.data;
         return {
-            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
         };
     }
 })({
@@ -37,34 +87,75 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
             const header = params.blockHeader || a.data.blocks[0].header;
             const block = a.data.blocks.find(b => b.header === header);
             if (!block) throw new Error(`Data block '${[header]}' not found.`);
-            const models = await Model.create(Format.mmCIF(block)).runInContext(ctx);
+            const models = await trajectoryFromMmCIF(block).runInContext(ctx);
             if (models.length === 0) throw new Error('No models found.');
-            const label = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
-            return new SO.Molecule.Trajectory(models, label);
+            const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
+
+type TrajectoryFromPDB = typeof TrajectoryFromPDB
+const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-pdb',
+    display: { name: 'Parse PDB', description: 'Parse PDB string and create trajectory.' },
+    from: [SO.Data.String],
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a }) {
+        return Task.create('Parse PDB', async ctx => {
+            const parsed = await parsePDB(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            const models = await trajectoryFromPDB(parsed.result).runInContext(ctx);
+            const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
+        });
+    }
+});
+
+type TrajectoryFromGRO = typeof TrajectoryFromGRO
+const TrajectoryFromGRO = PluginStateTransform.BuiltIn({
+    name: 'trajectory-from-gro',
+    display: { name: 'Parse GRO', description: 'Parse GRO string and create trajectory.' },
+    from: [SO.Data.String],
+    to: SO.Molecule.Trajectory
+})({
+    apply({ a }) {
+        return Task.create('Parse GRO', async ctx => {
+            const parsed = await parseGRO(a.data).runInContext(ctx);
+            if (parsed.isError) throw new Error(parsed.message);
+            const models = await trajectoryFromGRO(parsed.result).runInContext(ctx);
+            const props = { label: models[0].label, description: `${models.length} model${models.length === 1 ? '' : 's'}` };
+            return new SO.Molecule.Trajectory(models, props);
         });
     }
 });
 
-export { ModelFromTrajectory }
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     name: 'model-from-trajectory',
-    display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' },
+    display: { name: 'Molecular Model', description: 'Create a molecular model from specified index in a trajectory.' },
     from: SO.Molecule.Trajectory,
     to: SO.Molecule.Model,
-    params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) })
+    params: a => {
+        if (!a) {
+            return { modelIndex: PD.Numeric(0, {}, { description: 'Zero-based index of the model' }) };
+        }
+        return { modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }
+    }
 })({
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         const model = a.data[params.modelIndex];
-        const label = { label: `Model ${model.modelNum}` };
-        return new SO.Molecule.Model(model, label);
+        const props = a.data.length === 1
+            ? { label: `${model.label}` }
+            : { label: `${model.label}:${model.modelNum}`, description: `Model ${params.modelIndex + 1} of ${a.data.length}` };
+        return new SO.Molecule.Model(model, props);
     }
 });
 
-export { StructureFromModel }
 type StructureFromModel = typeof StructureFromModel
 const StructureFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-from-model',
@@ -74,8 +165,8 @@ const StructureFromModel = PluginStateTransform.BuiltIn({
 })({
     apply({ a }) {
         let s = Structure.ofModel(a.data);
-        const label = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` };
-        return new SO.Molecule.Structure(s, label);
+        const props = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` };
+        return new SO.Molecule.Structure(s, props);
     }
 });
 
@@ -83,7 +174,6 @@ function structureDesc(s: Structure) {
     return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
 }
 
-export { StructureAssemblyFromModel }
 type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
 const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-assembly-from-model',
@@ -91,64 +181,176 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     from: SO.Molecule.Model,
     to: SO.Molecule.Structure,
     params(a) {
+        if (!a) {
+            return { id: PD.Optional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. Value \'deposited\' can be used to specify deposited asymmetric unit.' })) };
+        }
         const model = a.data;
         const ids = model.symmetry.assemblies.map(a => [a.id, `${a.id}: ${stringToWords(a.details)}`] as [string, string]);
-        if (!ids.length) ids.push(['deposited', 'Deposited'])
-        return { id: PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' }) };
+        ids.push(['deposited', 'Deposited']);
+        return { id: PD.Optional(PD.Select(ids[0][0], ids, { label: 'Asm Id', description: 'Assembly Id' })) };
     }
 })({
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Build Assembly', async ctx => {
             const model = a.data;
-            const id = params.id;
-            const asm = ModelSymmetry.findAssembly(model, id);
-            if (id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`);
+            let id = params.id;
+            let asm: Assembly | undefined = void 0;
+
+            // if no id is specified, use the 1st assembly.
+            if (!id && model.symmetry.assemblies.length !== 0) {
+                id = model.symmetry.assemblies[0].id;
+            }
+
+            if (model.symmetry.assemblies.length === 0) {
+                if (id !== 'deposited') {
+                    plugin.log.warn(`Model '${a.data.label}' has no assembly, returning deposited structure.`);
+                }
+            } else {
+                asm = ModelSymmetry.findAssembly(model, id || '');
+                if (!asm) {
+                    plugin.log.warn(`Model '${a.data.label}' has no assembly called '${id}', returning deposited structure.`);
+                }
+            }
 
             const base = Structure.ofModel(model);
             if (!asm) {
-                plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`);
                 const label = { label: a.data.label, description: structureDesc(base) };
                 return new SO.Molecule.Structure(base, label);
             }
 
+            id = asm.id;
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
-            const label = { label: `Assembly ${id}`, description: structureDesc(s) };
-            return new SO.Molecule.Structure(s, label);
+            const props = { label: `Assembly ${id}`, description: structureDesc(s) };
+            return new SO.Molecule.Structure(s, props);
+        })
+    }
+});
+
+type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
+const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
+    name: 'structure-symmetry-from-model',
+    display: { name: 'Structure Symmetry', description: 'Create a molecular structure symmetry.' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Structure,
+    params(a) {
+        return {
+            ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
+            ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+        }
+    }
+})({
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Build Symmetry', async ctx => {
+            const { ijkMin, ijkMax } = params
+            const model = a.data;
+            const base = Structure.ofModel(model);
+            const s = await StructureSymmetry.buildSymmetryRange(base, ijkMin, ijkMax).runInContext(ctx);
+            const props = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: structureDesc(s) };
+            return new SO.Molecule.Structure(s, props);
         })
     }
 });
 
-export { StructureSelection }
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
-    display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified model.' },
+    display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified query expression.' },
     from: SO.Molecule.Structure,
     to: SO.Molecule.Structure,
     params: {
         query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }),
-        label: PD.makeOptional(PD.Text('', { isHidden: true }))
+        label: PD.Optional(PD.Text('', { isHidden: true }))
     }
 })({
-    apply({ a, params }) {
-        // TODO: use cache, add "update"
+    apply({ a, params, cache }) {
         const compiled = compile<Sel>(params.query);
+        (cache as { compiled: QueryFn<Sel> }).compiled = compiled;
+        (cache as { source: Structure }).source = a.data;
+
+        const result = compiled(new QueryContext(a.data));
+        const s = Sel.unionStructure(result);
+        if (s.elementCount === 0) return StateObject.Null;
+        const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) };
+        return new SO.Molecule.Structure(s, props);
+    },
+    update: ({ a, b, oldParams, newParams, cache }) => {
+        if (oldParams.query !== newParams.query) return StateTransformer.UpdateResult.Recreate;
+
+        if ((cache as { source: Structure }).source === a.data) {
+            return StateTransformer.UpdateResult.Unchanged;
+        }
+        (cache as { source: Structure }).source = a.data;
+
+        if (updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label)) {
+            return StateTransformer.UpdateResult.Updated;
+        }
+        return StateTransformer.UpdateResult.Null;
+    }
+});
+
+type UserStructureSelection = typeof UserStructureSelection
+const UserStructureSelection = PluginStateTransform.BuiltIn({
+    name: 'user-structure-selection',
+    display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified query expression.' },
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure,
+    params: {
+        query: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :residue-test (= atom.resname ALA))' }),
+        label: PD.Optional(PD.Text(''))
+    }
+})({
+    apply({ a, params, cache }) {
+        const parsed = parseMolScript(params.query.expression);
+        if (parsed.length === 0) throw new Error('No query');
+        const query = transpileMolScript(parsed[0]);
+        const compiled = compile<Sel>(query);
+        (cache as { compiled: QueryFn<Sel> }).compiled = compiled;
+        (cache as { source: Structure }).source = a.data;
         const result = compiled(new QueryContext(a.data));
         const s = Sel.unionStructure(result);
-        const label = { label: `${params.label || 'Selection'}`, description: structureDesc(s) };
-        return new SO.Molecule.Structure(s, label);
+        const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) };
+        return new SO.Molecule.Structure(s, props);
+    },
+    update: ({ a, b, oldParams, newParams, cache }) => {
+        if (oldParams.query.language !== newParams.query.language || oldParams.query.expression !== newParams.query.expression) {
+            return StateTransformer.UpdateResult.Recreate;
+        }
+
+        if ((cache as { source: Structure }).source === a.data) {
+            return StateTransformer.UpdateResult.Unchanged;
+        }
+        (cache as { source: Structure }).source = a.data;
+
+        updateStructureFromQuery((cache as { compiled: QueryFn<Sel> }).compiled, a.data, b, newParams.label);
+        return StateTransformer.UpdateResult.Updated;
     }
 });
 
-export { StructureComplexElement }
-namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' }
+function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.Molecule.Structure, label?: string) {
+    const result = query(new QueryContext(src));
+    const s = Sel.unionStructure(result);
+    if (s.elementCount === 0) {
+        return false;
+    }
+
+    obj.label = `${label || 'Selection'}`;
+    obj.description = structureDesc(s);
+    obj.data = s;
+    return true;
+}
+
+
+namespace StructureComplexElement {
+    export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
+}
+const StructureComplexElementTypes: [StructureComplexElement.Types, StructureComplexElement.Types][] = ['atomic-sequence', 'water', 'atomic-het', 'spheres'].map(t => [t, t] as any);
 type StructureComplexElement = typeof StructureComplexElement
 const StructureComplexElement = PluginStateTransform.BuiltIn({
     name: 'structure-complex-element',
     display: { name: 'Complex Element', description: 'Create a molecular structure from the specified model.' },
     from: SO.Molecule.Structure,
     to: SO.Molecule.Structure,
-    params: { type: PD.Text<StructureComplexElement.Types>('atomic-sequence', { isHidden: true }) }
+    params: { type: PD.Select<StructureComplexElement.Types>('atomic-sequence', StructureComplexElementTypes, { isHidden: true }) }
 })({
     apply({ a, params }) {
         // TODO: update function.
@@ -170,14 +372,16 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
     }
 });
 
-export { CustomModelProperties }
 type CustomModelProperties = typeof CustomModelProperties
 const CustomModelProperties = PluginStateTransform.BuiltIn({
     name: 'custom-model-properties',
     display: { name: 'Custom Model Properties' },
     from: SO.Molecule.Model,
     to: SO.Molecule.Model,
-    params: (a, ctx: PluginContext) => ({ properties: ctx.customModelProperties.getSelect(a.data) })
+    params: (a, ctx: PluginContext) => {
+        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
+        return { properties: ctx.customModelProperties.getSelect(a.data) };
+    }
 })({
     apply({ a, params }, ctx: PluginContext) {
         return Task.create('Custom Props', async taskCtx => {
diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts
index eef2b6d671de1d810c72fe26dada2309d4971817..e057d0d7c29401247b834a09d9ac7536fc326668 100644
--- a/src/mol-plugin/state/transforms/representation.ts
+++ b/src/mol-plugin/state/transforms/representation.ts
@@ -1,24 +1,44 @@
 /**
- * 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>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Transformer } from 'mol-state';
-import { Task } from 'mol-task';
-import { PluginStateTransform } from '../objects';
-import { PluginStateObject as SO } from '../objects';
+import { Structure } from 'mol-model/structure';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
 import { PluginContext } from 'mol-plugin/context';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { createTheme } from 'mol-theme/theme';
+import { RepresentationProvider } from 'mol-repr/representation';
 import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry';
-import { Structure } from 'mol-model/structure';
 import { StructureParams } from 'mol-repr/structure/representation';
+import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry';
+import { VolumeParams } from 'mol-repr/volume/representation';
+import { StateTransformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color';
+import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size';
+import { createTheme, ThemeRegistryContext } from 'mol-theme/theme';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+import { Text } from 'mol-geo/geometry/text/text';
+import { ColorNames } from 'mol-util/color/tables';
+import { getLabelRepresentation } from 'mol-plugin/util/structure-labels';
 import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+import { unwindStructureAssembly, explodeStructure, getStructureOverpaint } from '../animation/helpers';
+import { Color } from 'mol-util/color';
+import { Overpaint } from 'mol-theme/overpaint';
+
+export { StructureRepresentation3D }
+export { StructureRepresentation3DHelpers }
+export { StructureLabels3D}
+export { ExplodeStructureRepresentation3D }
+export { UnwindStructureAssemblyRepresentation3D }
+export { OverpaintStructureRepresentation3D as ColorStructureRepresentation3D }
+export { VolumeRepresentation3D }
 
-export namespace StructureRepresentation3DHelpers {
-    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
+namespace StructureRepresentation3DHelpers {
+    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
 
         const themeDataCtx = { structure };
@@ -32,7 +52,59 @@ export namespace StructureRepresentation3DHelpers {
         })
     }
 
-    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
+    export function createParams<R extends RepresentationProvider<Structure, any, any>, C extends ColorTheme.Provider<any>, S extends SizeTheme.Provider<any>>(
+            ctx: PluginContext, structure: Structure, params: {
+            repr?: R | [R, (r: R, ctx: ThemeRegistryContext, s: Structure) => RepresentationProvider.ParamValues<R>],
+            color?: C | [C, (c: C, ctx: ThemeRegistryContext) => ColorTheme.ParamValues<C>],
+            size?: S | [S, (c: S, ctx: ThemeRegistryContext) => SizeTheme.ParamValues<S>]
+        }): StateTransformer.Params<StructureRepresentation3D> {
+
+        const themeCtx = ctx.structureRepresentation.themeCtx
+
+        const repr = params.repr
+            ? params.repr instanceof Array ? params.repr[0] : params.repr
+            : ctx.structureRepresentation.registry.default.provider;
+        const reprParams = params.repr instanceof Array
+            ? params.repr[1](repr as R, themeCtx, structure)
+            : PD.getDefaultValues(repr.getParams(themeCtx, structure));
+
+        const color = params.color
+            ? params.color instanceof Array ? params.color[0] : params.color
+            : themeCtx.colorThemeRegistry.get(repr.defaultColorTheme);
+        const colorParams = params.color instanceof Array
+            ? params.color[1](color as C, themeCtx)
+            : PD.getDefaultValues(color.getParams(themeCtx));
+
+        const size = params.size
+            ? params.size instanceof Array ? params.size[0] : params.size
+            : themeCtx.sizeThemeRegistry.get(repr.defaultSizeTheme);
+        const sizeParams = params.size instanceof Array
+            ? params.size[1](size as S, themeCtx)
+            : PD.getDefaultValues(size.getParams(themeCtx));
+
+        return ({
+            type: { name: ctx.structureRepresentation.registry.getName(repr), params: reprParams },
+            colorTheme: { name: themeCtx.colorThemeRegistry.getName(color), params: colorParams },
+            sizeTheme: { name: themeCtx.sizeThemeRegistry.getName(size), params: sizeParams }
+        })
+    }
+
+    export function getDefaultParamsWithTheme(ctx: PluginContext, reprName: BuiltInStructureRepresentationsName, colorName: BuiltInColorThemeName | undefined, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
+        const type = ctx.structureRepresentation.registry.get(reprName);
+
+        const themeDataCtx = { structure };
+        const color = colorName || type.defaultColorTheme;
+        const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(color).getParams(themeDataCtx);
+        const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).getParams(themeDataCtx)
+        const structureDefaultParams = PD.getDefaultValues(type.getParams(ctx.structureRepresentation.themeCtx, structure))
+        return ({
+            type: { name: reprName, params: structureParams ? { ...structureDefaultParams, ...structureParams } : structureDefaultParams },
+            colorTheme: { name: color, params: PD.getDefaultValues(colorParams) },
+            sizeTheme: { name: type.defaultSizeTheme, params: PD.getDefaultValues(sizeParams) }
+        })
+    }
+
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
         const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
         const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues
@@ -43,16 +115,36 @@ export namespace StructureRepresentation3DHelpers {
         })
     }
 }
-export { StructureRepresentation3D }
+
 type StructureRepresentation3D = typeof StructureRepresentation3D
 const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'structure-representation-3d',
     display: '3D Representation',
     from: SO.Molecule.Structure,
-    to: SO.Molecule.Representation3D,
+    to: SO.Molecule.Structure.Representation3D,
     params: (a, ctx: PluginContext) => {
         const { registry, themeCtx } = ctx.structureRepresentation
         const type = registry.get(registry.default.name);
+
+        if (!a) {
+            return {
+                type: PD.Mapped<any>(
+                    registry.default.name,
+                    registry.types,
+                    name => PD.Group<any>(registry.get(name).getParams(themeCtx, Structure.Empty))),
+                colorTheme: PD.Mapped<any>(
+                    type.defaultColorTheme,
+                    themeCtx.colorThemeRegistry.types,
+                    name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ structure: Structure.Empty }))
+                ),
+                sizeTheme: PD.Mapped<any>(
+                    type.defaultSizeTheme,
+                    themeCtx.sizeThemeRegistry.types,
+                    name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ structure: Structure.Empty }))
+                )
+            }
+        }
+
         const dataCtx = { structure: a.data }
         return ({
             type: PD.Mapped<any>(
@@ -72,9 +164,9 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
         })
     }
 })({
-    canAutoUpdate({ oldParams, newParams }) {
-        // TODO: allow for small molecules
-        return oldParams.type.name === newParams.type.name;
+    canAutoUpdate({ a, oldParams, newParams }) {
+        // TODO: other criteria as well?
+        return a.data.elementCount < 10000 || oldParams.type.name === newParams.type.name;
     },
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
@@ -84,16 +176,299 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
             repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, params))
             // TODO set initial state, repr.setState({})
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Molecule.Representation3D(repr, { label: provider.label });
+            return new SO.Molecule.Structure.Representation3D({ repr, source: a } , { label: provider.label });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
-            if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate;
-            const props = { ...b.data.props, ...newParams.type.params }
-            b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams))
-            await b.data.createOrUpdate(props, a.data).runInContext(ctx);
-            return Transformer.UpdateResult.Updated;
+            if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
+            const props = { ...b.data.repr.props, ...newParams.type.params }
+            b.data.repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams));
+            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
+});
+
+
+type StructureLabels3D = typeof StructureLabels3D
+const StructureLabels3D = PluginStateTransform.BuiltIn({
+    name: 'structure-labels-3d',
+    display: '3D Labels',
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure.Representation3D,
+    params: {
+        // TODO: other targets
+        target: PD.MappedStatic('residues', {
+            'elements': PD.Group({ }),
+            'residues': PD.Group({ }),
+            'static-text': PD.Group({
+                value: PD.Text(''),
+                size: PD.Optional(PD.Numeric(1, { min: 1, max: 1000, step: 0.1 })),
+                // TODO: this changes the position while rotated etc... fix
+                position: PD.Optional(Text.Params.attachment)
+            }, { isFlat: true })
+        }),
+        options: PD.Group({
+            ...Text.Params,
+
+            background: PD.Boolean(true),
+            backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
+            backgroundColor: PD.Color(ColorNames.snow),
+            backgroundOpacity: PD.Numeric(0.9, { min: 0, max: 1, step: 0.01 }),
+        })
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return (oldParams.target.name === 'static-text' && newParams.target.name === 'static-text' && oldParams.target.params.value === newParams.target.params.value)
+            || newParams.target.name === oldParams.target.name;
+    },
+    apply({ a, params }) {
+        return Task.create('Structure Labels', async ctx => {
+            const repr = await getLabelRepresentation(ctx, a.data, params);
+            return new SO.Molecule.Structure.Representation3D({ repr, source: a }, { label: `Labels`, description: params.target.name });
+        });
+    },
+    update({ a, b, newParams }) {
+        return Task.create('Structure Labels', async ctx => {
+            await getLabelRepresentation(ctx, a.data, newParams, b.data.repr as ShapeRepresentation<any, any, any>);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
+});
+
+type UnwindStructureAssemblyRepresentation3D = typeof UnwindStructureAssemblyRepresentation3D
+const UnwindStructureAssemblyRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'unwind-structure-assembly-representation-3d',
+    display: 'Unwind Assembly 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: { t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.source.data;
+        const unitTransforms = new StructureUnitTransforms(structure);
+        unwindStructureAssembly(structure, unitTransforms, params.t);
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { unitTransforms },
+            initialState: { unitTransforms: new StructureUnitTransforms(structure) },
+            info: structure,
+            source: a
+        }, { label: `Unwind T = ${params.t.toFixed(2)}` });
+    },
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure;
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
+        if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
+        const unitTransforms = b.data.state.unitTransforms!;
+        unwindStructureAssembly(structure, unitTransforms, newParams.t);
+        b.label = `Unwind T = ${newParams.t.toFixed(2)}`;
+        b.data.source = a;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
+
+type ExplodeStructureRepresentation3D = typeof ExplodeStructureRepresentation3D
+const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'explode-structure-representation-3d',
+    display: 'Explode 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: { t: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }) }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params, spine }) {
+        const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data;
+        const structure = a.data.source.data;
+        const unitTransforms = new StructureUnitTransforms(rootStructure);
+        explodeStructure(structure, unitTransforms, params.t);
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { unitTransforms },
+            initialState: { unitTransforms: new StructureUnitTransforms(rootStructure) },
+            info: rootStructure,
+            source: a
+        }, { label: `Explode T = ${params.t.toFixed(2)}` });
+    },
+    update({ a, b, newParams, oldParams, spine }) {
+        const rootStructure = spine.getRootOfType(SO.Molecule.Structure)!.data;
+        if (b.data.info !== rootStructure) return StateTransformer.UpdateResult.Recreate;
+        if (oldParams.t === newParams.t) return StateTransformer.UpdateResult.Unchanged;
+        const unitTransforms = b.data.state.unitTransforms!;
+        explodeStructure(rootStructure, unitTransforms, newParams.t);
+        b.label = `Explode T = ${newParams.t.toFixed(2)}`;
+        b.data.source = a;
+        return StateTransformer.UpdateResult.Updated;
+    }
+});
+
+type OverpaintStructureRepresentation3D = typeof OverpaintStructureRepresentation3D
+const OverpaintStructureRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'overpaint-structure-representation-3d',
+    display: 'Overpaint 3D Representation',
+    from: SO.Molecule.Structure.Representation3D,
+    to: SO.Molecule.Structure.Representation3DState,
+    params: {
+        layers: PD.ObjectList({
+            script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :residue-test (= atom.resname LYS))' }),
+            color: PD.Color(ColorNames.blueviolet)
+        }, e => `${Color.toRgbString(e.color)}`, {
+            defaultValue: [
+                {
+                    script: {
+                        language: 'mol-script',
+                        expression: '(sel.atom.atom-groups :residue-test (= atom.resname LYS))'
+                    },
+                    color: ColorNames.blueviolet
+                },
+                {
+                    script: {
+                        language: 'mol-script',
+                        expression: '(sel.atom.atom-groups :residue-test (= atom.resname ALA))'
+                    },
+                    color: ColorNames.chartreuse
+                }
+            ]
+        }),
+        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity' }),
+    }
+})({
+    canAutoUpdate() {
+        return true;
+    },
+    apply({ a, params }) {
+        const structure = a.data.source.data
+        const overpaint = getStructureOverpaint(structure, params.layers, params.alpha)
+
+        return new SO.Molecule.Structure.Representation3DState({
+            state: { overpaint },
+            initialState: { overpaint: Overpaint.Empty },
+            info: structure,
+            source: a
+        }, { label: `Overpaint (${overpaint.layers.length} Layers)` })
+    },
+    update({ a, b, newParams, oldParams }) {
+        const structure = b.data.info as Structure
+        if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate
+        const oldOverpaint = b.data.state.overpaint!
+        const newOverpaint = getStructureOverpaint(structure, newParams.layers, newParams.alpha)
+        if (oldParams.alpha === newParams.alpha && Overpaint.areEqual(oldOverpaint, newOverpaint)) return StateTransformer.UpdateResult.Unchanged
+
+        b.data.state.overpaint = newOverpaint
+        b.data.source = a
+        b.label = `Overpaint (${newOverpaint.layers.length} Layers)`
+        return StateTransformer.UpdateResult.Updated
+    }
+});
+
+//
+
+export namespace VolumeRepresentation3DHelpers {
+    export function getDefaultParams(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volume: VolumeData, volumeParams?: Partial<PD.Values<VolumeParams>>): StateTransformer.Params<VolumeRepresentation3D> {
+        const type = ctx.volumeRepresentation.registry.get(name);
+
+        const themeDataCtx = { volume };
+        const colorParams = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).getParams(themeDataCtx);
+        const sizeParams = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).getParams(themeDataCtx)
+        const volumeDefaultParams = PD.getDefaultValues(type.getParams(ctx.volumeRepresentation.themeCtx, volume))
+        return ({
+            type: { name, params: volumeParams ? { ...volumeDefaultParams, ...volumeParams } : volumeDefaultParams },
+            colorTheme: { name: type.defaultColorTheme, params: PD.getDefaultValues(colorParams) },
+            sizeTheme: { name: type.defaultSizeTheme, params: PD.getDefaultValues(sizeParams) }
+        })
+    }
+
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volumeParams?: Partial<PD.Values<PD.Params>>, colorName?: BuiltInColorThemeName, colorParams?: Partial<ColorTheme.Props>, sizeName?: BuiltInSizeThemeName, sizeParams?: Partial<SizeTheme.Props>): StateTransformer.Params<VolumeRepresentation3D> {
+        const type = ctx.volumeRepresentation.registry.get(name);
+        const colorType = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(colorName || type.defaultColorTheme);
+        const sizeType = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(sizeName || type.defaultSizeTheme);
+        return ({
+            type: { name, params: volumeParams ? { ...type.defaultValues, ...volumeParams } : type.defaultValues },
+            colorTheme: { name: type.defaultColorTheme, params: colorParams ? { ...colorType.defaultValues, ...colorParams } : colorType.defaultValues },
+            sizeTheme: { name: type.defaultSizeTheme, params: sizeParams ? { ...sizeType.defaultValues, ...sizeParams } : sizeType.defaultValues }
+        })
+    }
+
+    export function getDescription(props: any) {
+        return props.isoValue && VolumeIsoValue.toString(props.isoValue)
+    }
+}
+type VolumeRepresentation3D = typeof VolumeRepresentation3D
+const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
+    name: 'volume-representation-3d',
+    display: '3D Representation',
+    from: SO.Volume.Data,
+    to: SO.Volume.Representation3D,
+    params: (a, ctx: PluginContext) => {
+        const { registry, themeCtx } = ctx.volumeRepresentation
+        const type = registry.get(registry.default.name);
+
+        if (!a) {
+            return {
+                type: PD.Mapped<any>(
+                    registry.default.name,
+                    registry.types,
+                    name => PD.Group<any>(registry.get(name).getParams(themeCtx, VolumeData.One ))),
+                colorTheme: PD.Mapped<any>(
+                    type.defaultColorTheme,
+                    themeCtx.colorThemeRegistry.types,
+                    name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ volume: VolumeData.One }))
+                ),
+                sizeTheme: PD.Mapped<any>(
+                    type.defaultSizeTheme,
+                    themeCtx.sizeThemeRegistry.types,
+                    name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ volume: VolumeData.One }))
+                )
+            }
+        }
+
+        const dataCtx = { volume: a.data }
+        return ({
+            type: PD.Mapped<any>(
+                registry.default.name,
+                registry.types,
+                name => PD.Group<any>(registry.get(name).getParams(themeCtx, a.data))),
+            colorTheme: PD.Mapped<any>(
+                type.defaultColorTheme,
+                themeCtx.colorThemeRegistry.getApplicableTypes(dataCtx),
+                name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams(dataCtx))
+            ),
+            sizeTheme: PD.Mapped<any>(
+                type.defaultSizeTheme,
+                themeCtx.sizeThemeRegistry.types,
+                name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams(dataCtx))
+            )
+        })
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        // TODO: allow for small molecules
+        return oldParams.type.name === newParams.type.name;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Volume Representation', async ctx => {
+            const provider = plugin.volumeRepresentation.registry.get(params.type.name)
+            const props = params.type.params || {}
+            const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
+            repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
+            // TODO set initial state, repr.setState({})
+            await repr.createOrUpdate(props, a.data).runInContext(ctx);
+            return new SO.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Volume Representation', async ctx => {
+            if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
+            const props = { ...b.data.repr.props, ...newParams.type.params }
+            b.data.repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
+            await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
+            b.description = VolumeRepresentation3DHelpers.getDescription(props)
+            return StateTransformer.UpdateResult.Updated;
         });
     }
 });
@@ -115,7 +490,7 @@ const ShapeRepresentation3D = PluginStateTransform.BuiltIn({
             const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils)
             // TODO set initial state, repr.setState({})
             await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
-            return new SO.Shape.Representation3D(repr, { label: a.data.label });
+            return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label });
         });
     }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/state/transforms/volume.ts b/src/mol-plugin/state/transforms/volume.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6861dc237dd801dfb0d7be565107e0840d545508
--- /dev/null
+++ b/src/mol-plugin/state/transforms/volume.ts
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2018-2019 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 CIF from 'mol-io/reader/cif';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
+import { Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+
+export { VolumeFromCcp4 };
+export { VolumeFromDsn6 };
+export { VolumeFromDensityServerCif };
+type VolumeFromCcp4 = typeof VolumeFromCcp4
+const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-ccp4',
+    display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' },
+    from: SO.Format.Ccp4,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
+            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDsn6 = typeof VolumeFromDsn6
+const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-dsn6',
+    display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' },
+    from: SO.Format.Dsn6,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from DSN6/BRIX', async ctx => {
+            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
+const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
+    name: 'volume-from-density-server-cif',
+    display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' },
+    from: SO.Format.Cif,
+    to: SO.Volume.Data,
+    params(a) {
+        if (!a) {
+            return {
+                blockHeader: PD.Optional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+            };
+        }
+        const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
+        return {
+            blockHeader: PD.Optional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+        };
+    }
+})({
+    isApplicable: a => a.data.blocks.length > 0,
+    apply({ a, params }) {
+        return Task.create('Parse density-server CIF', async ctx => {
+            const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data
+            const block = a.data.blocks.find(b => b.header === header);
+            if (!block) throw new Error(`Data block '${[header]}' not found.`);
+            const densityServerCif = CIF.schema.densityServer(block)
+            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx)
+            const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
\ No newline at end of file
diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx
index db43db77315ab37271bdd5b436115d5176272961..d4ae0171d84882a5f0c9fb6e2152b4fbfd5e2d83 100644
--- a/src/mol-plugin/ui/base.tsx
+++ b/src/mol-plugin/ui/base.tsx
@@ -10,7 +10,7 @@ import { PluginContext } from '../context';
 
 export const PluginReactContext = React.createContext(void 0 as any as PluginContext);
 
-export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> {
+export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> {
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
 
@@ -35,7 +35,7 @@ export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Com
     }
 }
 
-export abstract class PurePluginComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> {
+export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> {
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
 
diff --git a/src/mol-plugin/ui/camera.tsx b/src/mol-plugin/ui/camera.tsx
index ddcf5995d5fff6c62de49ea2e04d0f150da79a69..2d85a73149d5ad99778d9c1da883913d0a26fd27 100644
--- a/src/mol-plugin/ui/camera.tsx
+++ b/src/mol-plugin/ui/camera.tsx
@@ -6,21 +6,22 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
+import { Icon } from './controls/common';
 
-export class CameraSnapshots extends PluginComponent<{ }, { }> {
+export class CameraSnapshots extends PluginUIComponent<{ }, { }> {
     render() {
         return <div>
-            <div className='msp-section-header'>Camera Snapshots</div>
+            <div className='msp-section-header'><Icon name='code' /> Camera Snapshots</div>
             <CameraSnapshotControls />
             <CameraSnapshotList />
         </div>;
     }
 }
 
-class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> {
+class CameraSnapshotControls extends PluginUIComponent<{ }, { name: string, description: string }> {
     static Params = {
         name: PD.Text(),
         description: PD.Text()
@@ -48,7 +49,7 @@ class CameraSnapshotControls extends PluginComponent<{ }, { name: string, descri
     }
 }
 
-class CameraSnapshotList extends PluginComponent<{ }, { }> {
+class CameraSnapshotList extends PluginUIComponent<{ }, { }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate());
     }
@@ -65,7 +66,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> {
 
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-            {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}>
+            {this.plugin.state.cameraSnapshots.state.entries.valueSeq().map(e =><li key={e!.id}>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
                 <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx
index 9d5a802b96009435dadc2175cb8a9236034c8b9a..0b251fdc277c998271ca9a68334475f03ee59eb5 100644
--- a/src/mol-plugin/ui/controls.tsx
+++ b/src/mol-plugin/ui/controls.tsx
@@ -6,47 +6,247 @@
 
 import * as React from 'react';
 import { PluginCommands } from 'mol-plugin/command';
-import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
-import { PluginComponent } from './base';
+import { UpdateTrajectory } from 'mol-plugin/state/actions/structure';
+import { PluginUIComponent } from './base';
 import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager';
+import { IconButton } from './controls/common';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { StateTransformer } from 'mol-state';
+import { ModelFromTrajectory } from 'mol-plugin/state/transforms/model';
+import { AnimationControls } from './state/animation';
+
+export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
+    state = { show: false, label: '' }
+
+    private update = () => {
+        const state = this.plugin.state.dataState;
+
+        const models = state.selectQ(q => q.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
+
+        if (models.length === 0) {
+            this.setState({ show: false });
+            return;
+        }
+
+        let label = '', count = 0, parents = new Set<string>();
+        for (const m of models) {
+            if (!m.sourceRef) continue;
+            const parent = state.cells.get(m.sourceRef)!.obj as PluginStateObject.Molecule.Trajectory;
+
+            if (!parent) continue;
+            if (parent.data.length > 1) {
+                if (parents.has(m.sourceRef)) {
+                    // do not show the controls if there are 2 models of the same trajectory present
+                    this.setState({ show: false });
+                }
+
+                parents.add(m.sourceRef);
+                count++;
+                if (!label) {
+                    const idx = (m.transform.params! as StateTransformer.Params<ModelFromTrajectory>).modelIndex;
+                    label = `Model ${idx + 1} / ${parent.data.length}`;
+                }
+            }
+        }
+
+        if (count > 1) label = '';
+        this.setState({ show: count > 0, label });
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.state.dataState.events.changed, this.update);
+        this.subscribe(this.plugin.behaviors.state.isAnimating, this.update);
+    }
+
+    reset = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+        state: this.plugin.state.dataState,
+        action: UpdateTrajectory.create({ action: 'reset' })
+    });
+
+    prev = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+        state: this.plugin.state.dataState,
+        action: UpdateTrajectory.create({ action: 'advance', by: -1 })
+    });
+
+    next = () => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
+        state: this.plugin.state.dataState,
+        action: UpdateTrajectory.create({ action: 'advance', by: 1 })
+    });
+
+    // stopAnimation = () => {
+    //     this.plugin.state.animation.stop();
+    // }
+
+    // playAnimation = () => {
+    //     const anim = this.plugin.state.animation;
+    //     if (anim.state.params.current === AnimateModelIndex.name) {
+    //         anim.start();
+    //     } else {
+    //         anim.play(AnimateModelIndex, ParamDefinition.getDefaultValues(AnimateModelIndex.params(this.plugin) as any as ParamDefinition.Params))
+    //     }
+    // }
 
-export class Controls extends PluginComponent<{ }, { }> {
     render() {
-        return <>
+        const isAnimating = this.plugin.behaviors.state.isAnimating.value;
 
-        </>;
+        if (!this.state.show || (isAnimating && !this.state.label)) return null;
+
+        return <div className='msp-traj-controls'>
+            {/* <IconButton icon={isAnimating ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Play'} onClick={isAnimating ? this.stopAnimation : this.playAnimation} /> */}
+            {!isAnimating && <IconButton icon='model-first' title='First Model' onClick={this.reset} disabled={isAnimating} />}
+            {!isAnimating && <IconButton icon='model-prev' title='Previous Model' onClick={this.prev} disabled={isAnimating} />}
+            {!isAnimating && <IconButton icon='model-next' title='Next Model' onClick={this.next} disabled={isAnimating} />}
+            {!!this.state.label && <span>{this.state.label}</span> }
+        </div>;
     }
 }
 
-export class TrajectoryControls extends PluginComponent {
+export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBusy: boolean, show: boolean }> {
+    state = { isBusy: false, show: true }
+
+    componentDidMount() {
+        // TODO: this needs to be diabled when the state is updating!
+        this.subscribe(this.plugin.state.snapshots.events.changed, () => this.forceUpdate());
+        this.subscribe(this.plugin.behaviors.state.isUpdating, isBusy => this.setState({ isBusy }));
+        this.subscribe(this.plugin.behaviors.state.isAnimating, isBusy => this.setState({ isBusy }))
+
+        window.addEventListener('keyup', this.keyUp, false);
+    }
+
+    componentWillUnmount() {
+        super.componentWillUnmount();
+        window.removeEventListener('keyup', this.keyUp, false);
+    }
+
+    keyUp = (e: KeyboardEvent) => {
+        if (!e.ctrlKey || this.state.isBusy || e.target !== document.body) return;
+        const snapshots = this.plugin.state.snapshots;
+        if (e.keyCode === 37) { // left
+            if (snapshots.state.isPlaying) snapshots.stop();
+            this.prev();
+        } else if (e.keyCode === 38) { // up
+            if (snapshots.state.isPlaying) snapshots.stop();
+            if (snapshots.state.entries.size === 0) return;
+            const e = snapshots.state.entries.get(0);
+            this.update(e.snapshot.id);
+        } else if (e.keyCode === 39) { // right
+            if (snapshots.state.isPlaying) snapshots.stop();
+            this.next();
+        } else if (e.keyCode === 40) { // down
+            if (snapshots.state.isPlaying) snapshots.stop();
+            if (snapshots.state.entries.size === 0) return;
+            const e = snapshots.state.entries.get(snapshots.state.entries.size - 1);
+            this.update(e.snapshot.id);
+        }
+    };
+
+    async update(id: string) {
+        this.setState({ isBusy: true });
+        await PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
+        this.setState({ isBusy: false });
+    }
+
+    change = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        if (e.target.value === 'none') return;
+        this.update(e.target.value);
+    }
+
+    prev = () => {
+        const s = this.plugin.state.snapshots;
+        const id = s.getNextId(s.state.current, -1);
+        if (id) this.update(id);
+    }
+
+    next = () => {
+        const s = this.plugin.state.snapshots;
+        const id = s.getNextId(s.state.current, 1);
+        if (id) this.update(id);
+    }
+
+    togglePlay = () => {
+        this.plugin.state.snapshots.togglePlay();
+    }
+
     render() {
-        return <div>
-            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
-                state: this.plugin.state.dataState,
-                action: UpdateTrajectory.create({ action: 'advance', by: -1 })
-            })}>â—€</button>
-            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
-                state: this.plugin.state.dataState,
-                action: UpdateTrajectory.create({ action: 'reset' })
-            })}>↻</button>
-            <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
-                state: this.plugin.state.dataState,
-                action: UpdateTrajectory.create({ action: 'advance', by: +1 })
-            })}>â–º</button><br />
-        </div>
+        const snapshots = this.plugin.state.snapshots;
+        const count = snapshots.state.entries.size;
+
+        if (count < 2 || !this.state.show) {
+            return null;
+        }
+
+        const current = snapshots.state.current;
+        const isPlaying = snapshots.state.isPlaying;
+
+        return <div className='msp-state-snapshot-viewport-controls'>
+            <select className='msp-form-control' value={current || 'none'} onChange={this.change} disabled={this.state.isBusy || isPlaying}>
+                {!current && <option key='none' value='none'></option>}
+                {snapshots.state.entries.valueSeq().map((e, i) => <option key={e!.snapshot.id} value={e!.snapshot.id}>{`[${i! + 1}/${count}]`} {e!.name || new Date(e!.timestamp).toLocaleString()}</option>)}
+            </select>
+            <IconButton icon={isPlaying ? 'stop' : 'play'} title={isPlaying ? 'Pause' : 'Cycle States'} onClick={this.togglePlay}
+                disabled={isPlaying ? false : this.state.isBusy} />
+            {!isPlaying && <>
+                <IconButton icon='left-open' title='Previous State' onClick={this.prev} disabled={this.state.isBusy || isPlaying} />
+                <IconButton icon='right-open' title='Next State' onClick={this.next} disabled={this.state.isBusy || isPlaying} />
+            </>}
+        </div>;
     }
 }
 
-export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
+export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty: boolean, isExpanded: boolean, isUpdating: boolean, isAnimating: boolean, isPlaying: boolean }> {
+    state = { isEmpty: true, isExpanded: false, isUpdating: false, isAnimating: false, isPlaying: false };
+
+    componentDidMount() {
+        this.subscribe(this.plugin.state.snapshots.events.changed, () => {
+            if (this.plugin.state.snapshots.state.isPlaying) this.setState({ isPlaying: true, isExpanded: false });
+            else this.setState({ isPlaying: false });
+        });
+        this.subscribe(this.plugin.behaviors.state.isUpdating, isUpdating => {
+            if (isUpdating) this.setState({ isUpdating: true, isExpanded: false, isEmpty: this.plugin.state.dataState.tree.transforms.size < 2 });
+            else this.setState({ isUpdating: false, isEmpty: this.plugin.state.dataState.tree.transforms.size < 2 });
+        });
+        this.subscribe(this.plugin.behaviors.state.isAnimating, isAnimating => {
+            if (isAnimating) this.setState({ isAnimating: true, isExpanded: false });
+            else this.setState({ isAnimating: false });
+        });
+    }
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+    stop = () => {
+        this.plugin.state.animation.stop();
+        this.plugin.state.snapshots.stop();
+    }
+
+    render() {
+        // if (!this.state.show) return null;
+        const isPlaying = this.plugin.state.snapshots.state.isPlaying;
+        if (isPlaying) return null;
+
+        const isAnimating = this.state.isAnimating;
+
+        return <div className='msp-animation-viewport-controls'>
+            <IconButton icon={isAnimating || isPlaying ? 'stop' : 'play'} title={isAnimating ? 'Stop' : 'Select Animation'}
+                onClick={isAnimating || isPlaying ? this.stop : this.toggleExpanded}
+                disabled={isAnimating|| isPlaying ? false : this.state.isUpdating || this.state.isPlaying || this.state.isEmpty} />
+            {(this.state.isExpanded && !this.state.isUpdating) && <div className='msp-animation-viewport-controls-select'>
+                <AnimationControls onStart={this.toggleExpanded} />
+            </div>}
+        </div>;
+    }
+}
+
+export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
     state = { entries: [] }
 
     componentDidMount() {
-        this.subscribe(this.plugin.events.labels.highlight, e => this.setState({ entries: e.entries }));
+        this.subscribe(this.plugin.behaviors.labels.highlight, e => this.setState({ entries: e.entries }));
     }
 
     render() {
-        return <div style={{ textAlign: 'right' }}>
+        if (this.state.entries.length === 0) return null;
+
+        return <div className='msp-highlight-info'>
             {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)}
-        </div>
+        </div>;
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx
index 07e2c381325f0a39c936c3a174c068015183cd03..eea9c464af6de2f2299eea037c168a5ef5859843 100644
--- a/src/mol-plugin/ui/controls/common.tsx
+++ b/src/mol-plugin/ui/controls/common.tsx
@@ -4,12 +4,112 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import * as React from 'react';
+
+export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
+    state = { isExpanded: !!this.props.initialExpanded }
+
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
+    render() {
+        return <div className='msp-control-group-wrapper'>
+            <div className='msp-control-group-header'>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'collapse' : 'expand'}`} />
+                    {this.props.header}
+                </button>
+            </div>
+            {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                {this.props.children}
+            </div>
+            }
+        </div>
+    }
+}
+
+export class NumericInput extends React.PureComponent<{
+    value: number,
+    onChange: (v: number) => void,
+    onEnter?: () => void,
+    onBlur?: () => void,
+    blurOnEnter?: boolean,
+    isDisabled?: boolean,
+    placeholder?: string
+}, { value: string }> {
+    state = { value: '0' };
+    input = React.createRef<HTMLInputElement>();
+
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = +e.target.value;
+        this.setState({ value: e.target.value }, () => {
+            if (!Number.isNaN(value) && value !== this.props.value) {
+                this.props.onChange(value);
+            }
+        });
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if ((e.keyCode === 13 || e.charCode === 13)) {
+            if (this.props.blurOnEnter && this.input.current) {
+                this.input.current.blur();
+            }
+            if (this.props.onEnter) this.props.onEnter();
+        }
+    }
+
+    onBlur = () => {
+        this.setState({ value: '' + this.props.value });
+        if (this.props.onBlur) this.props.onBlur();
+    }
+
+    static getDerivedStateFromProps(props: { value: number }, state: { value: string }) {
+        const value = +state.value;
+        if (Number.isNaN(value) || value === props.value) return null;
+        return { value: '' + props.value };
+    }
+
+    render() {
+        return <input type='text'
+            ref={this.input}
+            onBlur={this.onBlur}
+            value={this.state.value}
+            placeholder={this.props.placeholder}
+            onChange={this.onChange}
+            onKeyPress={this.props.onEnter || this.props.blurOnEnter ? this.onKeyPress : void 0}
+            disabled={!!this.props.isDisabled}
+        />
+    }
+}
+
+export function Icon(props: {
+    name: string
+}) {
+    return <span className={`msp-icon msp-icon-${props.name}`} />;
+}
+
+export function IconButton(props: {
+    icon: string,
+    isSmall?: boolean,
+    onClick: (e: React.MouseEvent<HTMLButtonElement>) => void,
+    title?: string,
+    toggleState?: boolean,
+    disabled?: boolean,
+    'data-id'?: string
+}) {
+    let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}`;
+    if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
+    return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']}>
+        <span className={`msp-icon msp-icon-${props.icon}`}/>
+    </button>;
+}
+
+
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,
 //     value: boolean,
 //     label: string,
 //     title?: string
-// }) => <div className='lm-control-row lm-toggle-button' title={props.title}> 
+// }) => <div className='lm-control-row lm-toggle-button' title={props.title}>
 //         <span>{props.label}</span>
 //         <div>
 //             <button onClick={e => { props.onChange.call(null, !props.value); (e.target as HTMLElement).blur(); }}>
diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx
index 0674d2c1e80ceaafc893de64e468ad7af5e91952..72135f24d2a4cca1b01a47a69b98830547f9e292 100644
--- a/src/mol-plugin/ui/controls/parameters.tsx
+++ b/src/mol-plugin/ui/controls/parameters.tsx
@@ -1,11 +1,11 @@
 /**
- * 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>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Vec2 } from 'mol-math/linear-algebra';
+import { Vec2, Vec3 } from 'mol-math/linear-algebra';
 import { Color } from 'mol-util/color';
 import { ColorListName, getColorListFromName } from 'mol-util/color/scale';
 import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
@@ -15,6 +15,7 @@ import { camelCaseToWords } from 'mol-util/string';
 import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
+import { NumericInput, IconButton } from './common';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
@@ -28,15 +29,17 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
     render() {
         const params = this.props.params;
         const values = this.props.values;
-        return <div style={{ width: '100%' }}>
-            {Object.keys(params).map(key => {
+        const keys = Object.keys(params);
+        if (keys.length === 0) return null;
+        return <>
+            {keys.map(key => {
                 const param = params[key];
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 if (!Control) return null;
                 return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
-        </div>;
+        </>;
     }
 }
 
@@ -47,6 +50,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'number': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
             ? NumberRangeControl : NumberInputControl;
         case 'converted': return ConvertedControl;
+        case 'conditioned': return ConditionedControl;
         case 'multi-select': return MultiSelectControl;
         case 'color': return ColorControl;
         case 'color-scale': return ColorScaleControl;
@@ -59,6 +63,8 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'group': return GroupControl;
         case 'mapped': return MappedControl;
         case 'line-graph': return LineGraphControl;
+        case 'script-expression': return ScriptExpressionControl;
+        case 'object-list': return ObjectListControl;
         default:
             const _: never = param;
             console.warn(`${_} has no associated UI component`);
@@ -73,7 +79,7 @@ export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: strin
 export type ParamControl = React.ComponentClass<ParamProps<any>>
 
 export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> {
-    protected update(value: any) {
+    protected update(value: P['defaultValue']) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
@@ -108,20 +114,20 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
     }
 
     onHover = (point?: Vec2) => {
-        this.setState({isOverPoint: !this.state.isOverPoint});
+        this.setState({ isOverPoint: !this.state.isOverPoint });
         if (point) {
-            this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+            this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
             return;
         }
-        this.setState({message: `${this.props.value.length} points`});
+        this.setState({ message: `${this.props.value.length} points` });
     }
 
     onDrag = (point: Vec2) => {
-        this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+        this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
     }
 
-    onChange = (value: PD.LineGraph['defaultValue'] ) => {
-        this.props.onChange({ name: this.props.name, param: this.props.param, value: value});
+    onChange = (value: PD.LineGraph['defaultValue']) => {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
@@ -145,26 +151,39 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
                     data={this.props.param.defaultValue}
                     onChange={this.onChange}
                     onHover={this.onHover}
-                    onDrag={this.onDrag}/>
+                    onDrag={this.onDrag} />
             </div>
         </>;
     }
 }
 
-export class NumberInputControl extends SimpleParam<PD.Numeric> {
-    onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
-    renderControl() {
-        return <span>
-            number input TODO
-        </span>
+export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> {
+    state = { value: '0' };
+
+    update = (value: number) => {
+        this.props.onChange({ param: this.props.param, name: this.props.name, value });
+    }
+
+    render() {
+        const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        return <div className='msp-control-row'>
+            <span title={this.props.param.description}>{label}</span>
+            <div>
+                <NumericInput
+                    value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder}
+                    isDisabled={this.props.isDisabled} onChange={this.update} />
+            </div>
+        </div>;
     }
 }
 
 export class NumberRangeControl extends SimpleParam<PD.Numeric> {
     onChange = (v: number) => { this.update(v); }
     renderControl() {
-        return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
-            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />
+        const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value;
+        return <Slider value={value} min={this.props.param.min!} max={this.props.param.max!}
+            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />
     }
 }
 
@@ -204,8 +223,9 @@ export class SelectControl extends SimpleParam<PD.Select<string | number>> {
         }
     }
     renderControl() {
-        return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
-            {!this.props.param.options.some(e => e[0] === this.props.value) && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
+        const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
+        return <select value={this.props.value !== void 0 ? this.props.value : this.props.param.defaultValue} onChange={this.onChange} disabled={this.props.isDisabled}>
+            {isInvalid && <option key={this.props.value} value={this.props.value}>{`[Invalid] ${this.props.value}`}</option>}
             {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}
         </select>;
     }
@@ -222,7 +242,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
     onChange = (v: [number, number]) => { this.update(v); }
     renderControl() {
         return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
-            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />;
+            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
     }
 }
 
@@ -230,7 +250,7 @@ let _colors: React.ReactFragment | undefined = void 0;
 function ColorOptions() {
     if (_colors) return _colors;
     _colors = <>{Object.keys(ColorNames).map(name =>
-        <option key={name} value={(ColorNames as { [k: string]: Color})[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color})[name])}` }} >
+        <option key={name} value={(ColorNames as { [k: string]: Color })[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color })[name])}` }} >
             {name}
         </option>
     )}</>;
@@ -296,17 +316,49 @@ export class ColorScaleControl extends SimpleParam<PD.ColorScale<any>> {
     }
 }
 
-export class Vec3Control extends SimpleParam<PD.Vec3> {
-    // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    //     this.setState({ value: e.target.value });
-    //     this.props.onChange(e.target.value);
-    // }
+export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
 
-    renderControl() {
-        return <span>vec3 TODO</span>;
+    components = {
+        0: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
+        1: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
+        2: PD.Numeric(0, void 0, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
+    }
+
+    change(value: PD.MultiSelect<any>['defaultValue']) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    componentChange: ParamOnChange = ({ name, value }) => {
+        const v = Vec3.copy(Vec3.zero(), this.props.value);
+        v[+name] = value;
+        this.change(v);
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const v = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const value = `[${v[0].toFixed(2)}, ${v[1].toFixed(2)}, ${v[2].toFixed(2)}]`;
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>{value}</button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <ParameterControls params={this.components} values={v} onChange={this.componentChange} onEnter={this.props.onEnter} />
+            </div>
+        </>;
     }
 }
 
+
 export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
     change(value: File) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -329,7 +381,7 @@ export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 
-    change(value: PD.MultiSelect<any>['defaultValue'] ) {
+    change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
@@ -365,7 +417,8 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
                         <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
                             <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
                         </button>
-                </div> })}
+                    </div>
+                })}
             </div>
         </>;
     }
@@ -374,7 +427,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
 export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.param.isExpanded }
 
-    change(value: any ) {
+    change(value: any) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
@@ -386,6 +439,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
 
     render() {
         const params = this.props.param.params;
+
+        // Do not show if there are no params.
+        if (Object.keys(params).length === 0) return null;
+
         const label = this.props.param.label || camelCaseToWords(this.props.name);
 
         const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
@@ -410,16 +467,28 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
 }
 
 export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>> {
-    change(value: PD.Mapped<any>['defaultValue'] ) {
+    private valuesCache: { [name: string]: PD.Values<any> } = {}
+    private setValues(name: string, values: PD.Values<any>) {
+        this.valuesCache[name] = values
+    }
+    private getValues(name: string) {
+        if (name in this.valuesCache) {
+            return this.valuesCache[name]
+        } else {
+            return this.props.param.map(name).defaultValue
+        }
+    }
+
+    change(value: PD.Mapped<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
     onChangeName: ParamOnChange = e => {
-        // TODO: Cache values when changing types?
-        this.change({ name: e.value, params: this.props.param.map(e.value).defaultValue });
+        this.change({ name: e.value, params: this.getValues(e.value) });
     }
 
     onChangeParam: ParamOnChange = e => {
+        this.setValues(this.props.value.name, e.value)
         this.change({ name: this.props.value.name, params: e.value });
     }
 
@@ -437,10 +506,197 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
             return select;
         }
 
-        return <div>
+        return <>
             {select}
             <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
-        </div>
+        </>
+    }
+}
+
+type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
+type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
+
+class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> {
+    state = { params: {}, value: void 0 as any, current: void 0 as any };
+
+    onChangeParam: ParamOnChange = e => {
+        this.setState({ current: { ...this.state.current, [e.name]: e.value } });
+    }
+
+    apply = () => {
+        this.props.apply(this.state.current);
+    }
+
+    static getDerivedStateFromProps(props: _Props<ObjectListEditor>, state: _State<ObjectListEditor>): _State<ObjectListEditor> | null {
+        if (props.params === state.params && props.value === state.value) return null;
+        return {
+            params: props.params,
+            value: props.value,
+            current: props.value
+        };
+    }
+
+    render() {
+        return <>
+            <ParameterControls params={this.props.params} onChange={this.onChangeParam} values={this.state.current} onEnter={this.apply} isDisabled={this.props.isDisabled} />
+            <button className={`msp-btn msp-btn-block msp-form-control msp-control-top-offset`} onClick={this.apply} disabled={this.props.isDisabled}>
+                {this.props.isUpdate ? 'Update' : 'Add'}
+            </button>
+        </>;
+    }
+}
+
+class ObjectListItem extends React.PureComponent<{ param: PD.ObjectList, value: object, index: number, actions: ObjectListControl['actions'], isDisabled?: boolean }, { isExpanded: boolean }> {
+    state = { isExpanded: false };
+
+    update = (v: object) => {
+        this.setState({ isExpanded: false });
+        this.props.actions.update(v, this.props.index);
+    }
+
+    moveUp = () => {
+        this.props.actions.move(this.props.index, -1);
+    };
+
+    moveDown = () => {
+        this.props.actions.move(this.props.index, 1);
+    };
+
+    remove = () => {
+        this.setState({ isExpanded: false });
+        this.props.actions.remove(this.props.index);
+    };
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    };
+
+    static getDerivedStateFromProps(props: _Props<ObjectListEditor>, state: _State<ObjectListEditor>): _State<ObjectListEditor> | null {
+        if (props.params === state.params && props.value === state.value) return null;
+        return {
+            params: props.params,
+            value: props.value,
+            current: props.value
+        };
+    }
+
+    render() {
+        return <>
+            <div className='msp-param-object-list-item'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.toggleExpanded}>
+                    <span>{`${this.props.index + 1}: `}</span>
+                    {this.props.param.getLabel(this.props.value)}
+                </button>
+                <div>
+                    <IconButton icon='up-thin' title='Move Up' onClick={this.moveUp} isSmall={true} />
+                    <IconButton icon='down-thin' title='Move Down' onClick={this.moveDown} isSmall={true} />
+                    <IconButton icon='remove' title='Remove' onClick={this.remove} isSmall={true} />
+                </div>
+            </div>
+            {this.state.isExpanded && <div className='msp-control-offset'>
+                <ObjectListEditor params={this.props.param.element} apply={this.update} value={this.props.value} isUpdate isDisabled={this.props.isDisabled} />
+            </div>}
+        </>;
+    }
+}
+
+export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
+    state = { isExpanded: false }
+
+    change(value: any) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    add = (v: object) => {
+        this.change([...this.props.value, v]);
+    };
+
+    actions = {
+        update: (v: object, i: number) => {
+            const value = this.props.value.slice(0);
+            value[i] = v;
+            this.change(value);
+        },
+        move: (i: number, dir: -1 | 1) => {
+            let xs = this.props.value;
+            if (xs.length === 1) return;
+
+            let j = (i + dir) % xs.length;
+            if (j < 0) j += xs.length;
+
+            xs = xs.slice(0);
+            const t = xs[i];
+            xs[i] = xs[j];
+            xs[j] = t;
+            this.change(xs);
+        },
+        remove: (i: number) => {
+            const xs = this.props.value;
+            const update: object[] = [];
+            for (let j = 0; j < xs.length; j++) {
+                if (i !== j) update.push(xs[j]);
+            }
+            this.change(update);
+        }
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    };
+
+    render() {
+        const v = this.props.value;
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const value = `${v.length} item${v.length !== 1 ? 's' : ''}`;
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>{value}</button>
+                </div>
+            </div>
+            {this.state.isExpanded && <div className='msp-control-offset'>
+                {this.props.value.map((v, i) => <ObjectListItem key={i} param={this.props.param} value={v} index={i} actions={this.actions} />)}
+                <ObjectListEditor params={this.props.param.element} apply={this.add} value={this.props.param.ctor()} isDisabled={this.props.isDisabled} />
+            </div>}
+        </>;
+    }
+}
+
+export class ConditionedControl extends React.PureComponent<ParamProps<PD.Conditioned<any, any, any>>> {
+    change(value: PD.Conditioned<any, any, any>['defaultValue']) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    onChangeCondition: ParamOnChange = e => {
+        this.change(this.props.param.conditionedValue(this.props.value, e.value));
+    }
+
+    onChangeParam: ParamOnChange = e => {
+        this.change(e.value);
+    }
+
+    render() {
+        const value = this.props.value;
+        const condition = this.props.param.conditionForValue(value) as string
+        const param = this.props.param.conditionParams[condition];
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        const Conditioned = controlFor(param);
+
+        const select = <SelectControl param={this.props.param.select}
+            isDisabled={this.props.isDisabled} onChange={this.onChangeCondition} onEnter={this.props.onEnter}
+            name={`${label} Kind`} value={condition} />
+
+        if (!Conditioned) {
+            return select;
+        }
+
+        return <>
+            {select}
+            <Conditioned param={param} value={value} name={label} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
+        </>
     }
 }
 
@@ -461,3 +717,32 @@ export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converte
         return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
     }
 }
+
+export class ScriptExpressionControl extends SimpleParam<PD.ScriptExpression> {
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = e.target.value;
+        if (value !== this.props.value.expression) {
+            this.update({ language: this.props.value.language, expression: value });
+        }
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if (!this.props.onEnter) return;
+        if ((e.keyCode === 13 || e.charCode === 13)) {
+            this.props.onEnter();
+        }
+    }
+
+    renderControl() {
+        // TODO: improve!
+
+        const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
+        return <input type='text'
+            value={this.props.value.expression || ''}
+            placeholder={placeholder}
+            onChange={this.onChange}
+            onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
+            disabled={this.props.isDisabled}
+        />;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/ui/controls/slider.tsx b/src/mol-plugin/ui/controls/slider.tsx
index f930a20fe558d0496ba4eeec0689a97ad4bb506d..b57f3efc8d806a20518ad4b636596e0b2e88e353 100644
--- a/src/mol-plugin/ui/controls/slider.tsx
+++ b/src/mol-plugin/ui/controls/slider.tsx
@@ -5,6 +5,8 @@
  */
 
 import * as React from 'react'
+import { NumericInput } from './common';
+import { noop } from 'mol-util';
 
 export class Slider extends React.Component<{
     min: number,
@@ -12,7 +14,8 @@ export class Slider extends React.Component<{
     value: number,
     step?: number,
     onChange: (v: number) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: number }> {
 
     state = { isChanging: false, current: 0 }
@@ -35,18 +38,35 @@ export class Slider extends React.Component<{
         this.setState({ current });
     }
 
+    updateManually = (v: number) => {
+        this.setState({ isChanging: true });
+
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.props.min) n = this.props.min;
+        if (n > this.props.max) n = this.props.max;
+
+        this.setState({ current: n, isChanging: true });
+    }
+
+    onManualBlur = () => {
+        this.setState({ isChanging: false });
+        this.props.onChange(this.state.current);
+    }
+
     render() {
         let step = this.props.step;
         if (step === void 0) step = 1;
         return <div className='msp-slider'>
             <div>
-                <div>
-                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
-                        onBeforeChange={this.begin}
-                        onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
-                </div></div>
+                <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                    onBeforeChange={this.begin}
+                    onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
+            </div>
             <div>
-                {`${Math.round(100 * this.state.current) / 100}`}
+                <NumericInput
+                    value={this.state.current} blurOnEnter={true} onBlur={this.onManualBlur}
+                    isDisabled={this.props.disabled} onChange={this.updateManually} />
             </div>
         </div>;
     }
@@ -58,7 +78,8 @@ export class Slider2 extends React.Component<{
     value: [number, number],
     step?: number,
     onChange: (v: [number, number]) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: [number, number] }> {
 
     state = { isChanging: false, current: [0, 1] as [number, number] }
@@ -81,20 +102,41 @@ export class Slider2 extends React.Component<{
         this.setState({ current });
     }
 
+    updateMax = (v: number) => {
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.state.current[0]) n = this.state.current[0]
+        else if (n < this.props.min) n = this.props.min;
+        if (n > this.props.max) n = this.props.max;
+        this.props.onChange([this.state.current[0], n]);
+    }
+
+    updateMin = (v: number) => {
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.props.min) n = this.props.min;
+        if (n > this.state.current[1]) n = this.state.current[1];
+        else if (n > this.props.max) n = this.props.max;
+        this.props.onChange([n, this.state.current[1]]);
+    }
+
     render() {
         let step = this.props.step;
         if (step === void 0) step = 1;
         return <div className='msp-slider2'>
             <div>
-                {`${Math.round(100 * this.state.current[0]) / 100}`}
+                <NumericInput
+                    value={this.state.current[0]} onEnter={this.props.onEnter} blurOnEnter={true}
+                    isDisabled={this.props.disabled} onChange={this.updateMin} />
             </div>
             <div>
-                <div>
-                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
-                        onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} />
-                </div></div>
+                <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                    onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} />
+            </div>
             <div>
-                {`${Math.round(100 * this.state.current[1]) / 100}`}
+                <NumericInput
+                    value={this.state.current[1]} onEnter={this.props.onEnter} blurOnEnter={true}
+                    isDisabled={this.props.disabled} onChange={this.updateMax} />
             </div>
         </div>;
     }
@@ -102,10 +144,10 @@ export class Slider2 extends React.Component<{
 
 /**
  * The following code was adapted from react-components/slider library.
- * 
+ *
  * The MIT License (MIT)
  * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
- * 
+ *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
@@ -116,12 +158,12 @@ export class Slider2 extends React.Component<{
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
 
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
@@ -151,9 +193,6 @@ function classNames(_classes: { [name: string]: boolean | number }) {
     return classes.join(' ');
 }
 
-function noop() {
-}
-
 function isNotTouchEvent(e: TouchEvent) {
     return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0);
 }
@@ -540,7 +579,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         }
         return false;
 
-        // return this.state.bounds.some((x, i) => e.target 
+        // return this.state.bounds.some((x, i) => e.target
 
         // (
         //     //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i])
@@ -702,7 +741,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
             dragging: handle === i,
             index: i,
             key: i,
-            ref: (h: any) => this.handleElements.push(h)  //`handle-${i}`,
+            ref: (h: any) => this.handleElements.push(h)  // `handle-${i}`,
         }));
         if (!range) { handles.shift(); }
 
diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx
index 01166c0f6b3b46b9c679e829dda1938674938738..4951a7bbb571169115de743792125b56475cea5c 100644
--- a/src/mol-plugin/ui/plugin.tsx
+++ b/src/mol-plugin/ui/plugin.tsx
@@ -4,22 +4,21 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { List } from 'immutable';
+import { PluginState } from 'mol-plugin/state';
+import { formatTime } from 'mol-util';
+import { LogEntry } from 'mol-util/log-entry';
 import * as React from 'react';
 import { PluginContext } from '../context';
-import { StateTree } from './state-tree';
-import { Viewport, ViewportControls } from './viewport';
-import { Controls, TrajectoryControls, LociLabelControl } from './controls';
-import { PluginComponent, PluginReactContext } from './base';
-import { CameraSnapshots } from './camera';
+import { PluginReactContext, PluginUIComponent } from './base';
+import { LociLabelControl, TrajectoryViewportControls, StateSnapshotViewportControls, AnimationViewportControls } from './controls';
 import { StateSnapshots } from './state';
-import { List } from 'immutable';
-import { LogEntry } from 'mol-util/log-entry';
-import { formatTime } from 'mol-util';
+import { StateObjectActions } from './state/actions';
+import { StateTree } from './state/tree';
 import { BackgroundTaskProgress } from './task';
-import { ApplyActionContol } from './state/apply-action';
-import { PluginState } from 'mol-plugin/state';
+import { Viewport, ViewportControls } from './viewport';
+import { StateTransform } from 'mol-state';
 import { UpdateTransformContol } from './state/update-transform';
-import { StateObjectCell } from 'mol-state';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
 
@@ -33,44 +32,72 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
 
     render() {
         return <PluginReactContext.Provider value={this.props.plugin}>
-            <div className='msp-plugin'>
-                <div className='msp-plugin-content msp-layout-expanded'>
-                    <div className='msp-layout-hide-top'>
-                        {this.region('main', <ViewportWrapper />)}
-                        {this.region('left', <State />)}
-                        {this.region('right', <div className='msp-scrollable-container msp-right-controls'>
-                            <CurrentObject />
-                            <Controls />
-                            <CameraSnapshots />
-                            <StateSnapshots />
-                        </div>)}
-                        {this.region('bottom', <Log />)}
-                    </div>
+            <Layout />
+        </PluginReactContext.Provider>;
+    }
+}
+
+class Layout extends PluginUIComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
+    }
+
+    region(kind: 'left' | 'right' | 'bottom' | 'main', Element: React.ComponentClass) {
+        return <div className={`msp-layout-region msp-layout-${kind}`}>
+            <div className='msp-layout-static'>
+                <Element />
+            </div>
+        </div>;
+    }
+
+    render() {
+        const layout = this.plugin.layout.state;
+        const controls = (this.plugin.spec.layout && this.plugin.spec.layout.controls) || { };
+
+        return <div className='msp-plugin'>
+            <div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}>
+                <div className={layout.showControls ? 'msp-layout-hide-top' : 'msp-layout-hide-top msp-layout-hide-right msp-layout-hide-bottom msp-layout-hide-left'}>
+                    {this.region('main', ViewportWrapper)}
+                    {layout.showControls && controls.left !== 'none' && this.region('left', controls.left || State)}
+                    {layout.showControls && controls.right !== 'none' && this.region('right', controls.right || ControlsWrapper)}
+                    {layout.showControls && controls.bottom !== 'none' && this.region('bottom', controls.bottom || Log)}
                 </div>
             </div>
-        </PluginReactContext.Provider>;
+        </div>;
+    }
+}
+
+
+export class ControlsWrapper extends PluginUIComponent {
+    render() {
+        return <div className='msp-scrollable-container msp-right-controls'>
+            <CurrentObject />
+            {/* <AnimationControlsWrapper /> */}
+            {/* <CameraSnapshots /> */}
+            <StateSnapshots />
+        </div>;
     }
 }
 
-export class ViewportWrapper extends PluginComponent {
+export class ViewportWrapper extends PluginUIComponent {
     render() {
         return <>
             <Viewport />
-            <div style={{ position: 'absolute', left: '10px', top: '10px', color: 'white' }}>
-                <TrajectoryControls />
+            <div className='msp-viewport-top-left-controls'>
+                <AnimationViewportControls />
+                <TrajectoryViewportControls />
+                <StateSnapshotViewportControls />
             </div>
             <ViewportControls />
             <div style={{ position: 'absolute', left: '10px', bottom: '10px' }}>
                 <BackgroundTaskProgress />
             </div>
-            <div style={{ position: 'absolute', right: '10px', bottom: '10px' }}>
-                <LociLabelControl />
-            </div>
+            <LociLabelControl />
         </>;
     }
 }
 
-export class State extends PluginComponent {
+export class State extends PluginUIComponent {
     componentDidMount() {
         this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
     }
@@ -84,27 +111,30 @@ export class State extends PluginComponent {
         const kind = this.plugin.state.behavior.kind.value;
         return <div className='msp-scrollable-container'>
             <div className='msp-btn-row-group msp-data-beh'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}>
+                    <span className='msp-icon msp-icon-database' /> Data
+                </button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>
+                    <span className='msp-icon msp-icon-tools' /> Behavior
+                </button>
             </div>
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
         </div>
     }
 }
 
-export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
+export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
 
     componentDidMount() {
-        // TODO: only show last 100 entries.
-        this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) }));
+        this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries }));
     }
 
     componentDidUpdate() {
         this.scrollToBottom();
     }
 
-    state = { entries: List<LogEntry>() };
+    state = { entries: this.plugin.log.entries };
 
     private scrollToBottom() {
         const log = this.wrapper.current;
@@ -112,19 +142,26 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     }
 
     render() {
+        // TODO: ability to show full log
+        // showing more entries dramatically slows animations.
+        const maxEntries = 10;
+        const xs = this.state.entries, l = xs.size;
+        const entries: JSX.Element[] = [];
+        for (let i = Math.max(0, l - maxEntries), o = 0; i < l; i++) {
+            const e = xs.get(i);
+            entries.push(<li key={o++}>
+                <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
+                <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
+                <div className='msp-log-entry'>{e!.message}</div>
+            </li>);
+        }
         return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
-            <ul className='msp-list-unstyled'>
-                {this.state.entries.map((e, i) => <li key={i}>
-                    <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
-                    <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
-                    <div className='msp-log-entry'>{e!.message}</div>
-                </li>)}
-            </ul>
+            <ul className='msp-list-unstyled'>{entries}</ul>
         </div>;
     }
 }
 
-export class CurrentObject extends PluginComponent {
+export class CurrentObject extends PluginUIComponent {
     get current() {
         return this.plugin.state.behavior.currentObject.value;
     }
@@ -145,21 +182,19 @@ export class CurrentObject extends PluginComponent {
         const current = this.current;
         const ref = current.ref;
         const cell = current.state.cells.get(ref)!;
-        const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0;
-
-        const type = cell && cell.obj ? cell.obj.type : void 0;
         const transform = cell.transform;
-        const def = transform.transformer.definition;
 
-        const actions = type ? current.state.actions.fromType(type) : [];
+        let showActions = true;
+        if (ref === StateTransform.RootRef) {
+            const children = current.state.tree.children.get(ref);
+            showActions = children.size !== 0;
+        }
+
+        if (!showActions) return null;
+
         return <>
-            <div className='msp-section-header'>
-                {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name}
-            </div>
-            { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> }
-            {cell.status === 'ok' &&
-                actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)
-            }
+            {(cell.status === 'ok' || cell.status === 'error') && <UpdateTransformContol state={current.state} transform={transform} /> }
+            {cell.status === 'ok' && <StateObjectActions state={current.state} nodeRef={ref} initiallyColapsed />}
         </>;
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx
index a30896fb1fc4c12d4b5cf9736ab68324724cb0ae..a2a2f400dd2c103ccea6a4bfa504e4ca117bcaed 100644
--- a/src/mol-plugin/ui/state.tsx
+++ b/src/mol-plugin/ui/state.tsx
@@ -6,158 +6,303 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent, PurePluginUIComponent } from './base';
 import { shallowEqual } from 'mol-util';
-import { List } from 'immutable';
+import { OrderedMap } from 'immutable';
 import { ParameterControls } from './controls/parameters';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
-import { Subject } from 'rxjs';
+import { PluginState } from 'mol-plugin/state';
+import { urlCombine } from 'mol-util/url';
+import { IconButton, Icon } from './controls/common';
+import { formatTimespan } from 'mol-util/now';
 
-export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> {
-    state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' }
+export class StateSnapshots extends PluginUIComponent<{ }> {
+    downloadToFile = () => {
+        PluginCommands.State.Snapshots.DownloadToFile.dispatch(this.plugin, { });
+    }
+
+    open = (e: React.ChangeEvent<HTMLInputElement>) => {
+        if (!e.target.files || !e.target.files![0]) return;
 
-    updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
+        PluginCommands.State.Snapshots.OpenFile.dispatch(this.plugin, { file: e.target.files![0] });
+    }
 
     render() {
         return <div>
-            <div className='msp-section-header'>State Snapshots</div>
-            <StateSnapshotControls serverUrl={this.state.serverUrl} serverChanged={this.updateServerUrl} />
+            <div className='msp-section-header'><Icon name='code' /> State</div>
+            <LocalStateSnapshots />
             <LocalStateSnapshotList />
-            <RemoteStateSnapshotList serverUrl={this.state.serverUrl} />
+            <RemoteStateSnapshots />
+
+            <div className='msp-btn-row-group' style={{ marginTop: '10px' }}>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.downloadToFile}>Download JSON</button>
+                <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file'>
+                    {'Open JSON'} <input onChange={this.open} type='file' multiple={false} accept='.json' />
+                </div>
+            </div>
         </div>;
     }
 }
 
-// TODO: this is not nice: device some custom event system.
-const UploadedEvent = new Subject();
+class LocalStateSnapshots extends PluginUIComponent<
+    { },
+    { params: PD.Values<typeof LocalStateSnapshots.Params> }> {
 
-class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> {
-    state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
+    state = { params: PD.getDefaultValues(LocalStateSnapshots.Params) };
 
     static Params = {
         name: PD.Text(),
-        description: PD.Text(),
-        serverUrl: PD.Text()
-    }
+        options: PD.Group({
+            description: PD.Text(),
+            ...PluginState.GetSnapshotParams
+        })
+    };
 
     add = () => {
-        PluginCommands.State.Snapshots.Add.dispatch(this.plugin, { name: this.state.name, description: this.state.description });
-        this.setState({ name: '', description: '' })
+        PluginCommands.State.Snapshots.Add.dispatch(this.plugin, {
+            name: this.state.params.name,
+            description: this.state.params.options.description,
+            params: this.state.params.options
+        });
+        this.setState({
+            params: {
+                name: '',
+                options: {
+                    ...this.state.params.options,
+                    description: ''
+                }
+            }
+        });
     }
 
     clear = () => {
         PluginCommands.State.Snapshots.Clear.dispatch(this.plugin, {});
     }
 
-    shouldComponentUpdate(nextProps: { serverUrl: string, serverChanged: (url: string) => void }, nextState: { name: string, description: string, serverUrl: string, isUploading: boolean }) {
+    shouldComponentUpdate(nextProps: any, nextState: any) {
         return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
     }
 
-    upload = async () => {
-        this.setState({ isUploading: true });
-        await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl });
-        this.setState({ isUploading: false });
-        this.plugin.log.message('Snapshot uploaded.');
-        UploadedEvent.next();
-    }
-
     render() {
+        // TODO: proper styling
         return <div>
-            <ParameterControls params={StateSnapshotControls.Params} values={this.state} onEnter={this.add} onChange={p => {
-                this.setState({ [p.name]: p.value } as any);
-                if (p.name === 'serverUrl') this.props.serverChanged(p.value);
+            <ParameterControls params={LocalStateSnapshots.Params} values={this.state.params} onEnter={this.add} onChange={p => {
+                const params = { ...this.state.params, [p.name]: p.value };
+                this.setState({ params } as any);
+                this.plugin.state.snapshots.currentGetSnapshotParams = params.options;
             }}/>
 
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Add Local</button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='floppy' /> Save</button>
+                {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button> */}
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
             </div>
         </div>;
     }
 }
 
-class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
+class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
     }
 
-    apply(id: string) {
-        return () => PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
+    apply = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Apply.dispatch(this.plugin, { id });
     }
 
-    remove(id: string) {
-        return () => {
-            PluginCommands.State.Snapshots.Remove.dispatch(this.plugin, { id });
-        }
+    remove = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Remove.dispatch(this.plugin, { id });
+    }
+
+    moveUp = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Move.dispatch(this.plugin, { id, dir: -1 });
+    }
+
+    moveDown = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Move.dispatch(this.plugin, { id, dir: 1 });
+    }
+
+    replace = (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        PluginCommands.State.Snapshots.Replace.dispatch(this.plugin, { id, params: this.plugin.state.snapshots.currentGetSnapshotParams });
     }
 
     render() {
+        const current = this.plugin.state.snapshots.state.current;
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-            {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
-                <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
-                    <span className='msp-icon msp-icon-remove' />
+            {this.plugin.state.snapshots.state.entries.map(e => <li key={e!.snapshot.id}>
+                <button data-id={e!.snapshot.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.apply}>
+                    <span style={{ fontWeight: e!.snapshot.id === current ? 'bold' : void 0}}>
+                        {e!.name || new Date(e!.timestamp).toLocaleString()}</span> <small>
+                        {`${e!.snapshot.durationInMs ? formatTimespan(e!.snapshot.durationInMs, false) + `${e!.description ? ', ' : ''}` : ''}${e!.description ? e!.description : ''}`}
+                    </small>
                 </button>
+                <div>
+                    <IconButton data-id={e!.snapshot.id} icon='up-thin' title='Move Up' onClick={this.moveUp} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='down-thin' title='Move Down' onClick={this.moveDown} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='switch' title='Replace' onClick={this.replace} isSmall={true} />
+                    <IconButton data-id={e!.snapshot.id} icon='remove' title='Remove' onClick={this.remove} isSmall={true} />
+                </div>
             </li>)}
         </ul>;
     }
 }
 
-type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string }
-class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
-    state = { entries: List<RemoteEntry>(), isFetching: false };
+type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string, isSticky?: boolean }
+class RemoteStateSnapshots extends PluginUIComponent<
+    { },
+    { params: PD.Values<typeof RemoteStateSnapshots.Params>, entries: OrderedMap<string, RemoteEntry>, isBusy: boolean }> {
+
+    state = { params: PD.getDefaultValues(RemoteStateSnapshots.Params), entries: OrderedMap<string, RemoteEntry>(), isBusy: false };
+
+    static Params = {
+        name: PD.Text(),
+        options: PD.Group({
+            description: PD.Text(),
+            playOnLoad: PD.Boolean(false),
+            serverUrl: PD.Text('https://webchem.ncbr.muni.cz/molstar-state')
+        })
+    };
 
     componentDidMount() {
-        this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
         this.refresh();
-        this.subscribe(UploadedEvent, this.refresh);
+        // this.subscribe(UploadedEvent, this.refresh);
+    }
+
+    serverUrl(q?: string) {
+        if (!q) return this.state.params.options.serverUrl;
+        return urlCombine(this.state.params.options.serverUrl, q);
     }
 
     refresh = async () => {
         try {
-            this.setState({ isFetching: true });
-            const req = await fetch(`${this.props.serverUrl}/list`);
-            const json: RemoteEntry[] = await req.json();
-            this.setState({
-                entries: List<RemoteEntry>(json.map((e: RemoteEntry) => ({
+            this.setState({ isBusy: true });
+            const json = (await this.plugin.runTask<RemoteEntry[]>(this.plugin.fetch({ url: this.serverUrl('list'), type: 'json'  }))) || [];
+
+            json.sort((a, b) => {
+                if (a.isSticky === b.isSticky) return a.timestamp - b.timestamp;
+                return a.isSticky ? -1 : 1;
+            });
+
+            const entries = OrderedMap<string, RemoteEntry>().asMutable();
+            for (const e of json) {
+                entries.set(e.id, {
                     ...e,
-                    url: `${this.props.serverUrl}/get/${e.id}`,
-                    removeUrl: `${this.props.serverUrl}/remove/${e.id}`
-                }))),
-                isFetching: false })
+                    url: this.serverUrl(`get/${e.id}`),
+                    removeUrl: this.serverUrl(`remove/${e.id}`)
+                });
+            }
+
+            this.setState({ entries: entries.asImmutable(), isBusy: false })
         } catch (e) {
             this.plugin.log.error('Fetching Remote Snapshots: ' + e);
-            this.setState({ entries: List<RemoteEntry>(), isFetching: false })
+            this.setState({ entries: OrderedMap(), isBusy: false })
         }
     }
 
-    fetch(url: string) {
-        return () => PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url });
+    upload = async () => {
+        this.setState({ isBusy: true });
+        if (this.plugin.state.snapshots.state.entries.size === 0) {
+            await PluginCommands.State.Snapshots.Add.dispatch(this.plugin, {
+                name: this.state.params.name,
+                description: this.state.params.options.description,
+                params: this.plugin.state.snapshots.currentGetSnapshotParams
+            });
+        }
+
+        await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, {
+            name: this.state.params.name,
+            description: this.state.params.options.description,
+            playOnLoad: this.state.params.options.playOnLoad,
+            serverUrl: this.state.params.options.serverUrl
+        });
+        this.setState({ isBusy: false });
+        this.plugin.log.message('Snapshot uploaded.');
+        this.refresh();
     }
 
-    remove(url: string) {
-        return async () => {
-            this.setState({ entries: List() });
-            try {
-                await fetch(url);
-            } catch { }
-            this.refresh();
+    fetch = async (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        const entry = this.state.entries.get(id);
+        if (!entry) return;
+
+        this.setState({ isBusy: true });
+        try {
+            await PluginCommands.State.Snapshots.Fetch.dispatch(this.plugin, { url: entry.url });
+        } finally {
+            this.setState({ isBusy: false });
         }
     }
 
+    remove = async (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        const entry = this.state.entries.get(id);
+        if (!entry) return;
+        this.setState({ entries: this.state.entries.remove(id) });
+
+        try {
+            await fetch(entry.removeUrl);
+        } catch { }
+    }
+
     render() {
         return <div>
-            <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control msp-section-header' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button>
-
-            <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-                {this.state.entries.valueSeq().map(e =><li key={e!.id}>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.fetch(e!.url)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
-                    <button onClick={this.remove(e!.removeUrl)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
-                        <span className='msp-icon msp-icon-remove' />
-                    </button>
-                </li>)}
-            </ul>
+            <div className='msp-section-header'><Icon name='code' /> Remote State</div>
+
+            <ParameterControls params={RemoteStateSnapshots.Params} values={this.state.params} onEnter={this.upload} onChange={p => {
+                this.setState({ params: { ...this.state.params, [p.name]: p.value } } as any);
+            }} isDisabled={this.state.isBusy}/>
+
+            <div className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isBusy}><Icon name='upload' /> Upload</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isBusy}>Refresh</button>
+            </div>
+
+            <RemoteStateSnapshotList entries={this.state.entries} isBusy={this.state.isBusy} serverUrl={this.state.params.options.serverUrl}
+                fetch={this.fetch} remove={this.remove} />
         </div>;
     }
-}
\ No newline at end of file
+}
+
+class RemoteStateSnapshotList extends PurePluginUIComponent<
+    { entries: OrderedMap<string, RemoteEntry>, serverUrl: string, isBusy: boolean, fetch: (e: React.MouseEvent<HTMLElement>) => void, remove: (e: React.MouseEvent<HTMLElement>) => void },
+    { }> {
+
+    open = async (e: React.MouseEvent<HTMLElement>) => {
+        const id = e.currentTarget.getAttribute('data-id');
+        if (!id) return;
+        const entry = this.props.entries.get(id);
+        if (!entry) return;
+
+        e.preventDefault();
+        let url = `${window.location}`, qi = url.indexOf('?');
+        if (qi > 0) url = url.substr(0, qi);
+
+        window.open(`${url}?snapshot-url=${encodeURIComponent(entry.url)}`, '_blank');
+    }
+
+    render() {
+        return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
+            {this.props.entries.valueSeq().map(e =><li key={e!.id}>
+                <button data-id={e!.id} className='msp-btn msp-btn-block msp-form-control' onClick={this.props.fetch}
+                    disabled={this.props.isBusy} onContextMenu={this.open} title='Click to download, right-click to open in a new tab.'>
+                    {e!.name || new Date(e!.timestamp).toLocaleString()} <small>{e!.description}</small>
+                </button>
+                {!e!.isSticky && <div>
+                    <IconButton data-id={e!.id} icon='remove' title='Remove' onClick={this.props.remove} disabled={this.props.isBusy} />
+                </div>}
+            </li>)}
+        </ul>;
+    }
+}
diff --git a/src/mol-plugin/ui/state/actions.tsx b/src/mol-plugin/ui/state/actions.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a56467f7f3c6f4005d84ad8cd229fd613ae0111e
--- /dev/null
+++ b/src/mol-plugin/ui/state/actions.tsx
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2018 - 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 '../base';
+import { ApplyActionContol } from './apply-action';
+import { State } from 'mol-state';
+import { Icon } from '../controls/common';
+
+export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRef: string, hideHeader?: boolean, initiallyColapsed?: boolean }> {
+    get current() {
+        return this.plugin.state.behavior.currentObject.value;
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.state.behavior.currentObject, o => {
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+            const current = this.current;
+            if (current.ref !== ref || current.state !== state) return;
+            this.forceUpdate();
+        });
+    }
+
+    render() {
+        const { state, nodeRef: ref } = this.props;
+        const cell = state.cells.get(ref)!;
+        const actions = state.actions.fromCell(cell, this.plugin);
+        if (actions.length === 0) return null;
+
+        const def = cell.transform.transformer.definition;
+        const display = cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name;
+
+        return <div className='msp-state-actions'>
+            {!this.props.hideHeader && <div className='msp-section-header'><Icon name='code' /> {`Actions (${display})`}</div> }
+            {actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={state} action={act} nodeRef={ref} initiallyCollapsed={this.props.initiallyColapsed} />)}
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/ui/state/animation.tsx b/src/mol-plugin/ui/state/animation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e4cd317b493fce9af3e64c9f8e4ed07959cca64b
--- /dev/null
+++ b/src/mol-plugin/ui/state/animation.tsx
@@ -0,0 +1,63 @@
+/**
+ * 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 '../base';
+import { ParameterControls, ParamOnChange } from '../controls/parameters';
+import { Icon } from '../controls/common';
+
+export class AnimationControlsWrapper extends PluginUIComponent<{ }> {
+    render() {
+        const anim = this.plugin.state.animation;
+        if (anim.isEmpty) return null;
+        return <div className='msp-contols-section'>
+            <div className='msp-section-header'><Icon name='code' /> Animations</div>
+            <AnimationControls />
+        </div>
+    }
+}
+
+export class AnimationControls extends PluginUIComponent<{ onStart?: () => void }> {
+    componentDidMount() {
+        this.subscribe(this.plugin.state.animation.events.updated, () => this.forceUpdate());
+    }
+
+    updateParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateParams({ [p.name]: p.value });
+    }
+
+    updateCurrentParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateCurrentParams({ [p.name]: p.value });
+    }
+
+    startOrStop = () => {
+        const anim = this.plugin.state.animation;
+        if (anim.state.animationState === 'playing') anim.stop();
+        else {
+            if (this.props.onStart) this.props.onStart();
+            anim.start();
+        }
+    }
+
+    render() {
+        const anim = this.plugin.state.animation;
+        if (anim.isEmpty) return null;
+
+        const isDisabled = anim.state.animationState === 'playing';
+
+        return <>
+            <ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} />
+            <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} />
+
+            <div className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}>
+                    {anim.state.animationState !== 'playing' && <Icon name='play' />}
+                    {anim.state.animationState === 'playing' ? 'Stop' : 'Start'}
+                </button>
+            </div>
+        </>;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx
index 17482d70072e34909f0e0545b9c6e8986366a6e0..e4747abcb041de9eb2d1a3ef29cf9ee3e060b68d 100644
--- a/src/mol-plugin/ui/state/apply-action.tsx
+++ b/src/mol-plugin/ui/state/apply-action.tsx
@@ -6,8 +6,7 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginContext } from 'mol-plugin/context';
-import { State, Transform } from 'mol-state';
-import { StateAction } from 'mol-state/action';
+import { State, StateTransform, StateAction } from 'mol-state';
 import { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -17,13 +16,14 @@ export { ApplyActionContol };
 namespace ApplyActionContol {
     export interface Props {
         plugin: PluginContext,
-        nodeRef: Transform.Ref,
+        nodeRef: StateTransform.Ref,
         state: State,
-        action: StateAction
+        action: StateAction,
+        initiallyCollapsed?: boolean
     }
 
     export interface ComponentState {
-        ref: Transform.Ref,
+        ref: StateTransform.Ref,
         version: string,
         params: any,
         error?: string,
@@ -41,15 +41,16 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
         });
     }
     getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); }
+    getTransformerId() { return this.props.state.transforms.get(this.props.nodeRef).transformer.id; }
     getHeader() { return this.props.action.definition.display; }
     canApply() { return !this.state.error && !this.state.busy; }
     canAutoApply() { return false; }
     applyText() { return 'Apply'; }
     isUpdate() { return false; }
 
-    private _getInfo = memoizeLatest((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
+    private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 
-    state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
+    state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false, isCollapsed: this.props.initiallyCollapsed };
 
     static getDerivedStateFromProps(props: ApplyActionContol.Props, state: ApplyActionContol.ComponentState) {
         if (props.nodeRef === state.ref) return null;
diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx
index a56615a85ba7e4579d5eb6626b204189726539f5..d46edc26580442335f813e9ea340f6cd0fc3fdd1 100644
--- a/src/mol-plugin/ui/state/common.tsx
+++ b/src/mol-plugin/ui/state/common.tsx
@@ -4,18 +4,18 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State, Transform, Transformer } from 'mol-state';
+import { State, StateTransform, StateTransformer, StateAction } from 'mol-state';
 import * as React from 'react';
-import { PurePluginComponent } from '../base';
+import { PurePluginUIComponent } from '../base';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
-import { StateAction } from 'mol-state/action';
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
+import { Icon } from '../controls/common';
 
 export { StateTransformParameters, TransformContolBase };
 
-class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
+class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> {
     validate(params: any) {
         // TODO
         return void 0;
@@ -61,7 +61,7 @@ namespace StateTransformParameters {
         return true;
     }
 
-    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] {
+    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: StateTransform.Ref): Props['info'] {
         const source = state.cells.get(nodeRef)!.obj!;
         const params = action.definition.params ? action.definition.params(source, plugin) : { };
         const initialValues = PD.getDefaultValues(params);
@@ -72,7 +72,7 @@ namespace StateTransformParameters {
         };
     }
 
-    export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] {
+    export function infoFromTransform(plugin: PluginContext, state: State, transform: StateTransform): Props['info'] {
         const cell = state.cells.get(transform.ref)!;
         // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
         // const create = transform.transformer.definition.params;
@@ -88,7 +88,7 @@ namespace StateTransformParameters {
 }
 
 namespace TransformContolBase {
-    export interface ControlState {
+    export interface ComponentState {
         params: any,
         error?: string,
         busy: boolean,
@@ -97,11 +97,12 @@ namespace TransformContolBase {
     }
 }
 
-abstract class TransformContolBase<P, S extends TransformContolBase.ControlState> extends PurePluginComponent<P, S> {
+abstract class TransformContolBase<P, S extends TransformContolBase.ComponentState> extends PurePluginUIComponent<P, S> {
     abstract applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
-    abstract getHeader(): Transformer.Definition['display'];
+    abstract getHeader(): StateTransformer.Definition['display'];
     abstract canApply(): boolean;
+    abstract getTransformerId(): string;
     abstract canAutoApply(newParams: any): boolean;
     abstract applyText(): string;
     abstract isUpdate(): boolean;
@@ -166,24 +167,42 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
 
     render() {
         const info = this.getInfo();
-        if (info.isEmpty && this.isUpdate()) return null;
+        const isEmpty = info.isEmpty && this.isUpdate();
 
         const display = this.getHeader();
 
-        return <div className='msp-transform-wrapper'>
+        const tId = this.getTransformerId();
+        const ParamEditor: StateTransformParameters.Class = this.plugin.customParamEditors.has(tId)
+            ? this.plugin.customParamEditors.get(tId)!
+            : StateTransformParameters;
+
+        const wrapClass = this.state.isCollapsed
+            ? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
+            : 'msp-transform-wrapper';
+        // this.isUpdate()
+        //     ? !isEmpty && !this.state.isCollapsed
+        //     ? 'msp-transform-update-wrapper'
+        //     : 'msp-transform-update-wrapper-collapsed'
+        //     : 'msp-transform-wrapper';
+
+        return <div className={wrapClass}>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button>
-                {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>}
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded} title={display.description}>
+                    {display.name}
+                    {/* {!isEmpty && this.state.isCollapsed && this.isUpdate() && <small>Click to Edit</small>} */}
+                </button>
             </div>
-            {!this.state.isCollapsed && <>
-                <StateTransformParameters info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
+            {!isEmpty && !this.state.isCollapsed && <>
+                <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
 
                 <div className='msp-transform-apply-wrap'>
+                    <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'><Icon name='cw' /></button>
                     <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
-                        ↶ Reset
+                        <Icon name='back' /> Back
                     </button>
                     <div className='msp-transform-apply'>
                         <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-${this.canApply() ? 'on' : 'off'}`} onClick={this.apply} disabled={!this.canApply()}>
+                            {this.canApply() && <Icon name='ok' />}
                             {this.applyText()}
                         </button>
                     </div>
diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state/tree.tsx
similarity index 54%
rename from src/mol-plugin/ui/state-tree.tsx
rename to src/mol-plugin/ui/state/tree.tsx
index 87123f35abcb6f93a966933e488c5815939f8328..f2c4ac4f805505441a4f14a1b9e387b64cdf1a5a 100644
--- a/src/mol-plugin/ui/state-tree.tsx
+++ b/src/mol-plugin/ui/state/tree.tsx
@@ -1,23 +1,47 @@
 /**
- * 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 * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { State, StateObject } from 'mol-state'
+import { State, StateObject, StateTransform } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from '../base';
+import { StateObjectActions } from './actions';
+
+export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
+    state = { showActions: true };
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.state.cell.created, e => {
+            if (e.cell.transform.parent === StateTransform.RootRef) this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.events.state.cell.removed, e => {
+            if (e.parent === StateTransform.RootRef) this.forceUpdate();
+        });
+    }
+
+    static getDerivedStateFromProps(props: { state: State }, state: { showActions: boolean }) {
+        const n = props.state.tree.root.ref;
+        const children = props.state.tree.children.get(n);
+        const showActions = children.size === 0;
+        if (state.showActions === showActions) return null;
+        return { showActions };
+    }
 
-export class StateTree extends PluginComponent<{ state: State }> {
     render() {
-        const n = this.props.state.tree.root.ref;
-        return <StateTreeNode state={this.props.state} nodeRef={n} />;
+        const ref = this.props.state.tree.root.ref;
+        if (this.state.showActions) {
+            return <StateObjectActions state={this.props.state} nodeRef={ref} hideHeader={true} />
+        }
+        return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />;
     }
 }
 
-class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCollapsed: boolean }> {
+class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> {
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
@@ -60,24 +84,37 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, {
     }
 
     render() {
-        if (this.props.state.cells.get(this.props.nodeRef)!.obj === StateObject.Null) return null;
+        const cell = this.props.state.cells.get(this.props.nodeRef);
+        if (!cell || cell.obj === StateObject.Null) return null;
 
         const cellState = this.cellState;
-
+        const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost;
         const children = this.props.state.tree.children.get(this.props.nodeRef);
-        return <div>
-            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} />
+        const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
+
+        if (!showLabel) {
+            if (children.size === 0) return null;
+            return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
+                {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+            </div>;
+        }
+
+        return <>
+            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} />
             {children.size === 0
                 ? void 0
-                : <div className='msp-tree-children' style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}
+                : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
+                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
                 </div>
             }
-        </div>;
+        </>;
     }
 }
 
-class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCurrent: boolean, isCollapsed: boolean }> {
+class StateTreeNodeLabel extends PluginUIComponent<
+    { nodeRef: string, state: State, depth: number },
+    { state: State, isCurrent: boolean, isCollapsed: boolean /*, updaterCollapsed: boolean */ }> {
+
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
@@ -107,7 +144,8 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
     state = {
         isCurrent: this.props.state.current === this.props.nodeRef,
         isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state
+        state: this.props.state,
+        // updaterCollapsed: true
     }
 
     static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) {
@@ -115,18 +153,20 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         return {
             isCurrent: props.state.current === props.nodeRef,
             isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
+            state: props.state,
+            updaterCollapsed: true
         };
     }
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
+        e.currentTarget.blur();
         PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
     }
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
@@ -153,9 +193,17 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         e.currentTarget.blur();
     }
 
+    // private toggleUpdaterObs = new Subject();
+    // toggleUpdater = (e: React.MouseEvent<HTMLAnchorElement>) => {
+    //     e.preventDefault();
+    //     e.currentTarget.blur();
+    //     this.toggleUpdaterObs.next();
+    // }
+
     render() {
         const n = this.props.state.transforms.get(this.props.nodeRef)!;
-        const cell = this.props.state.cells.get(this.props.nodeRef)!;
+        const cell = this.props.state.cells.get(this.props.nodeRef);
+        if (!cell) return null;
 
         const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
 
@@ -166,31 +214,68 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
             label = <><b>[{cell.status}]</b> <span title={name}>{name}</span></>;
         } else if (cell.status !== 'ok' || !cell.obj) {
             const name = n.transformer.definition.display.name;
-            const title = `${cell.errorText}`
-            label = <><b>[{cell.status}]</b> <a title={title} href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>;
+            const title = `${cell.errorText}`;
+            label = <><a title={title} href='#' onClick={this.setCurrent}><b>[{cell.status}]</b> {name}: <i>{cell.errorText}</i> </a></>;
         } else {
             const obj = cell.obj as PluginStateObject.Any;
             const title = `${obj.label} ${obj.description ? obj.description : ''}`
-            label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
+            if (this.state.isCurrent) {
+                label = <><a title={title} href='#'><b>{obj.label}</b> {obj.description ? <small>{obj.description}</small> : void 0}</a></>;
+            } else {
+                label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label} {obj.description ? <small>{obj.description}</small> : void 0}</a></>;
+            }
         }
 
         const children = this.props.state.tree.children.get(this.props.nodeRef);
         const cellState = this.props.state.cellStates.get(this.props.nodeRef);
 
-        const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
-            <span className='msp-icon msp-icon-remove' />
-        </button>;
-
         const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
             <span className='msp-icon msp-icon-visual-visibility' />
         </button>;
 
-        return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
-            {isCurrent ? <b>{label}</b> : label}
+        const style: React.HTMLAttributes<HTMLDivElement>['style'] = {
+            marginLeft: /* this.state.isCurrent ? void 0 :*/ `${this.props.depth * 10}px`,
+            // paddingLeft: !this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`,
+            borderLeft: /* isCurrent || */ this.props.depth === 0 ? 'none' : void 0
+        }
+
+        const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={style}>
+            {label}
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
-            {remove}{visibility}
-        </div>
+            {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+                <span className='msp-icon msp-icon-remove' />
+            </button>}{visibility}
+        </div>;
+
+        // if (this.state.isCurrent) {
+        //     return <>
+        //         {row}
+        //         <StateTreeNodeTransform {...this.props} toggleCollapsed={this.toggleUpdaterObs} />
+        //     </>
+        // }
+
+        return row;
     }
-}
\ No newline at end of file
+}
+
+// class StateTreeNodeTransform extends PluginUIComponent<{ nodeRef: string, state: State, depth: number, toggleCollapsed?: Observable<any> }> {
+//     componentDidMount() {
+//         // this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+//         //     if (this.props.nodeRef !== ref || this.props.state !== state) return;
+//         //     this.forceUpdate();
+//         // });
+//     }
+
+//     render() {
+//         const ref = this.props.nodeRef;
+//         const cell = this.props.state.cells.get(ref)!;
+//         const parent: StateObjectCell | undefined = (cell.sourceRef && this.props.state.cells.get(cell.sourceRef)!) || void 0;
+
+//         if (!parent || parent.status !== 'ok') return null;
+
+//         const transform = cell.transform;
+//         return <UpdateTransformContol state={this.props.state} transform={transform} initiallyCollapsed={true} toggleCollapsed={this.props.toggleCollapsed} />;
+//     }
+// }
\ No newline at end of file
diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx
index c7e89a1f43d4ba812913cecfc5572aa82787d822..e6c0bc198bfdb2d7abf330d1aaab0b07a223a13d 100644
--- a/src/mol-plugin/ui/state/update-transform.tsx
+++ b/src/mol-plugin/ui/state/update-transform.tsx
@@ -4,30 +4,30 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State, Transform } from 'mol-state';
+import { State, StateTransform } from 'mol-state';
 import { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
+import { Observable } from 'rxjs';
 
 export { UpdateTransformContol };
 
 namespace UpdateTransformContol {
     export interface Props {
-        transform: Transform,
-        state: State
+        transform: StateTransform,
+        state: State,
+        toggleCollapsed?: Observable<any>,
+        initiallyCollapsed?: boolean
     }
 
-    export interface ComponentState {
-        transform: Transform,
-        params: any,
-        error?: string,
-        busy: boolean,
-        isInitial: boolean
+    export interface ComponentState extends TransformContolBase.ComponentState {
+        transform: StateTransform
     }
 }
 
 class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Props, UpdateTransformContol.ComponentState> {
     applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); }
     getInfo() { return this._getInfo(this.props.transform); }
+    getTransformerId() { return this.props.transform.transformer.id; }
     getHeader() { return this.props.transform.transformer.definition.display; }
     canApply() { return !this.state.error && !this.state.busy && !this.state.isInitial; }
     applyText() { return this.canApply() ? 'Update' : 'Nothing to Update'; }
@@ -45,9 +45,23 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr
         return autoUpdate({ a: cell.obj!, b: parentCell.obj!, oldParams: this.getInfo().initialValues, newParams }, this.plugin);
     }
 
-    private _getInfo = memoizeLatest((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
+    componentDidMount() {
+        if (super.componentDidMount) super.componentDidMount();
 
-    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
+        if (this.props.toggleCollapsed) this.subscribe(this.props.toggleCollapsed, () => this.setState({ isCollapsed: !this.state.isCollapsed }));
+
+        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+            if (this.props.transform.ref !== ref || this.props.state !== state) return;
+            if (this.state.params !== this.props.transform.params) {
+                this._getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t));
+                this.setState({ params: this.props.transform.params, isInitial: true })
+            }
+        });
+    }
+
+    private _getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t));
+
+    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false, isCollapsed: this.props.initiallyCollapsed };
 
     static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) {
         if (props.transform === state.transform) return null;
diff --git a/src/mol-plugin/ui/task.tsx b/src/mol-plugin/ui/task.tsx
index c45b783ea05824070df202b5e24712b37cf61c09..20b1c7f14a072a0276ef709d4893c9f0785e8f8f 100644
--- a/src/mol-plugin/ui/task.tsx
+++ b/src/mol-plugin/ui/task.tsx
@@ -5,13 +5,13 @@
  */
 
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { OrderedMap } from 'immutable';
 import { TaskManager } from 'mol-plugin/util/task-manager';
 import { filter } from 'rxjs/operators';
 import { Progress } from 'mol-task';
 
-export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> {
+export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => {
             this.setState({ tracked: this.state.tracked.set(e.id, e) })
@@ -30,7 +30,7 @@ export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: Orde
     }
 }
 
-class ProgressEntry extends PluginComponent<{ event: TaskManager.ProgressEvent }> {
+class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> {
     render() {
         const root = this.props.event.progress.root;
         const subtaskCount = countSubtasks(this.props.event.progress.root) - 1;
diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx
index 6ccfd15a86170b8b4acea6c2437ae8d95ec88f97..859d1e421ad81d05154e84f66728cebdb638ed27 100644
--- a/src/mol-plugin/ui/viewport.tsx
+++ b/src/mol-plugin/ui/viewport.tsx
@@ -6,22 +6,21 @@
  */
 
 import * as React from 'react';
-import { ButtonsType } from 'mol-util/input/input-observer';
-import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { PluginCommands } from 'mol-plugin/command';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { Canvas3DParams } from 'mol-canvas3d/canvas3d';
+import { PluginLayoutStateParams } from 'mol-plugin/layout';
+import { ControlGroup, IconButton } from './controls/common';
 
 interface ViewportState {
     noWebGl: boolean
 }
 
-export class ViewportControls extends PluginComponent {
+export class ViewportControls extends PluginUIComponent<{}, { isSettingsExpanded: boolean }> {
     state = {
-        isSettingsExpanded: false,
-        settings: PD.getDefaultValues(Canvas3DParams)
+        isSettingsExpanded: false
     }
 
     resetCamera = () => {
@@ -33,40 +32,57 @@ export class ViewportControls extends PluginComponent {
         e.currentTarget.blur();
     }
 
-    // hideSettings = () => {
-    //     this.setState({ isSettingsExpanded: false });
-    // }
+    toggleControls = () => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.state.showControls } });
+    }
+
+    toggleExpanded = () => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } });
+    }
 
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { [p.name]: p.value } });
     }
 
+    setLayout = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
+    }
+
     componentDidMount() {
-        if (this.plugin.canvas3d) {
-            this.setState({ settings: this.plugin.canvas3d.props });
-        }
+        this.subscribe(this.plugin.events.canvas3d.settingsUpdated, e => {
+            this.forceUpdate();
+        });
 
-        this.subscribe(this.plugin.events.canvad3d.settingsUpdated, e => {
-            this.setState({ settings: this.plugin.canvas3d.props });
+        this.subscribe(this.plugin.layout.events.updated, () => {
+            this.forceUpdate();
         });
     }
 
+    icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
+        return <IconButton icon={name} toggleState={isOn} onClick={onClick} title={title} />;
+    }
+
     render() {
-        // TODO: show some icons dimmed etc..
         return <div className={'msp-viewport-controls'}>
             <div className='msp-viewport-controls-buttons'>
-                <button className='msp-btn msp-btn-link' onClick={this.toggleSettingsExpanded}><span className='msp-icon msp-icon-settings'/></button>
-                <button className='msp-btn msp-btn-link' title='Reset Camera' onClick={this.resetCamera}><span className='msp-icon msp-icon-reset-scene'/></button>
+                {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}<br/>
+                {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}<br/>
+                {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)}<br />
+                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
             </div>
-            {this.state.isSettingsExpanded &&
-            <div className='msp-viewport-controls-scene-options'>
-                <ParameterControls params={Canvas3DParams} values={this.state.settings} onChange={this.setSettings} />
+            {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'>
+                <ControlGroup header='Layout' initialExpanded={true}>
+                    <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} />
+                </ControlGroup>
+                <ControlGroup header='Viewport' initialExpanded={true}>
+                    <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
+                </ControlGroup>
             </div>}
         </div>
     }
 }
 
-export class Viewport extends PluginComponent<{ }, ViewportState> {
+export class Viewport extends PluginUIComponent<{ }, ViewportState> {
     private container = React.createRef<HTMLDivElement>();
     private canvas = React.createRef<HTMLCanvasElement>();
 
@@ -75,7 +91,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
     };
 
     private handleResize = () => {
-         this.plugin.canvas3d.handleResize();
+        this.plugin.canvas3d.handleResize();
     }
 
     componentDidMount() {
@@ -87,20 +103,10 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
         const canvas3d = this.plugin.canvas3d;
         this.subscribe(canvas3d.input.resize, this.handleResize);
 
-        const idHelper = new Canvas3dIdentifyHelper(this.plugin, 15);
-
-        this.subscribe(canvas3d.input.move, ({x, y, inside, buttons}) => {
-            if (!inside || buttons) { return; }
-            idHelper.move(x, y);
-        });
-
-        this.subscribe(canvas3d.input.leave, () => {
-            idHelper.leave();
-        });
-
-        this.subscribe(canvas3d.input.click, ({x, y, buttons}) => {
-            if (buttons !== ButtonsType.Flag.Primary) return;
-            idHelper.select(x, y);
+        this.subscribe(canvas3d.interaction.click, e => this.plugin.behaviors.canvas3d.click.next(e));
+        this.subscribe(canvas3d.interaction.highlight, e => this.plugin.behaviors.canvas3d.highlight.next(e));
+        this.subscribe(this.plugin.layout.events.updated, () => {
+            setTimeout(this.handleResize, 50);
         });
     }
 
diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts
deleted file mode 100644
index 74562d92b5c968166cd0f2346af1d2ef92c0ef55..0000000000000000000000000000000000000000
--- a/src/mol-plugin/util/canvas3d-identify.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { PluginContext } from '../context';
-import { PickingId } from 'mol-geo/geometry/picking';
-import { EmptyLoci, Loci } from 'mol-model/loci';
-import { Representation } from 'mol-repr/representation';
-
-export class Canvas3dIdentifyHelper {
-    private cX = -1;
-    private cY = -1;
-
-    private lastX = -1;
-    private lastY = -1;
-
-    private id: PickingId | undefined = void 0;
-
-    private currentIdentifyT = 0;
-
-    private prevLoci: { loci: Loci, repr?: Representation.Any } = { loci: EmptyLoci };
-    private prevT = 0;
-
-    private inside = false;
-
-    private async identify(select: boolean, t: number) {
-        if (this.lastX !== this.cX && this.lastY !== this.cY) {
-            this.id = await this.ctx.canvas3d.identify(this.cX, this.cY);
-            this.lastX = this.cX;
-            this.lastY = this.cY;
-        }
-
-        if (!this.id) return;
-
-        if (select) {
-            this.ctx.behaviors.canvas.selectLoci.next(this.ctx.canvas3d.getLoci(this.id));
-            return;
-        }
-
-        // only highlight the latest
-        if (!this.inside || this.currentIdentifyT !== t) {
-            return;
-        }
-
-        const loci = this.ctx.canvas3d.getLoci(this.id);
-        if (loci.repr !== this.prevLoci.repr || !Loci.areEqual(loci.loci, this.prevLoci.loci)) {
-            this.ctx.behaviors.canvas.highlightLoci.next(loci);
-            this.prevLoci = loci;
-        }
-    }
-
-    private animate: (t: number) => void = t => {
-        if (this.inside && t - this.prevT > 1000 / this.maxFps) {
-            this.prevT = t;
-            this.currentIdentifyT = t;
-            this.identify(false, t);
-        }
-        requestAnimationFrame(this.animate);
-    }
-
-    leave() {
-        this.inside = false;
-        if (this.prevLoci.loci !== EmptyLoci) {
-            this.prevLoci = { loci: EmptyLoci };
-            this.ctx.behaviors.canvas.highlightLoci.next(this.prevLoci);
-            this.ctx.canvas3d.requestDraw(true);
-        }
-    }
-
-    move(x: number, y: number) {
-        this.inside = true;
-        this.cX = x;
-        this.cY = y;
-    }
-
-    select(x: number, y: number) {
-        this.cX = x;
-        this.cY = y;
-        this.identify(true, 0);
-    }
-
-    constructor(private ctx: PluginContext, private maxFps: number = 15) {
-        this.animate(0);
-    }
-}
\ No newline at end of file
diff --git a/src/mol-plugin/util/loci-label-manager.ts b/src/mol-plugin/util/loci-label-manager.ts
index a747b640894b9229b2549e2e1b6cfb9985285ca1..9475f0b6f63d380c3f76466cd801f74cdb291b26 100644
--- a/src/mol-plugin/util/loci-label-manager.ts
+++ b/src/mol-plugin/util/loci-label-manager.ts
@@ -24,7 +24,7 @@ export class LociLabelManager {
     }
 
     private empty: any[] = [];
-    private getInfo(loci: Loci, repr?: Representation<any>) {
+    private getInfo({ loci, repr }: Representation.Loci) {
         if (!loci || loci.kind === 'empty-loci') return this.empty;
         const info: LociLabelEntry[] = [];
         for (let p of this.providers) {
@@ -35,6 +35,6 @@ export class LociLabelManager {
     }
 
     constructor(public ctx: PluginContext) {
-        ctx.behaviors.canvas.highlightLoci.subscribe(ev => ctx.events.labels.highlight.next({ entries: this.getInfo(ev.loci, ev.repr) }));
+        ctx.behaviors.canvas3d.highlight.subscribe(ev => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(ev.current) }));
     }
 }
\ No newline at end of file
diff --git a/src/mol-plugin/util/structure-element-selection.ts b/src/mol-plugin/util/structure-element-selection.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f990cfa79e2d0399801cb8a157619d22cbb911d5
--- /dev/null
+++ b/src/mol-plugin/util/structure-element-selection.ts
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { OrderedSet } from 'mol-data/int';
+import { EmptyLoci, Loci } from 'mol-model/loci';
+import { Structure, StructureElement } from 'mol-model/structure';
+import { StateObject } from 'mol-state';
+import { PluginContext } from '../context';
+import { PluginStateObject } from '../state/objects';
+
+export { StructureElementSelectionManager };
+
+class StructureElementSelectionManager {
+    private entries = new Map<string, SelectionEntry>();
+
+    private getEntry(s: Structure) {
+        const cell = this.plugin.helpers.substructureParent.get(s);
+        if (!cell) return;
+        const ref = cell.transform.ref;
+        if (!this.entries.has(ref)) {
+            const entry = SelectionEntry(s);
+            this.entries.set(ref, entry);
+            return entry;
+        }
+
+        return this.entries.get(ref)!;
+    }
+
+    add(loci: StructureElement.Loci): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry.selection = StructureElement.Loci.union(entry.selection, loci);
+        return entry.selection;
+    }
+
+    remove(loci: StructureElement.Loci): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry.selection = StructureElement.Loci.subtract(entry.selection, loci);
+        return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
+    }
+
+    set(loci: StructureElement.Loci): Loci {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return EmptyLoci;
+        entry.selection = loci;
+        return entry.selection.elements.length === 0 ? EmptyLoci : entry.selection;
+    }
+
+    clear() {
+        const keys = this.entries.keys();
+        const selections: StructureElement.Loci[] = [];
+        while (true) {
+            const k = keys.next();
+            if (k.done) break;
+            const s = this.entries.get(k.value)!;
+            if (s.selection.elements.length > 0) selections.push(s.selection);
+            s.selection = StructureElement.Loci(s.selection.structure, []);
+        }
+        return selections;
+    }
+
+    get(structure: Structure) {
+        const entry = this.getEntry(structure);
+        if (!entry) return EmptyLoci;
+        return entry.selection;
+    }
+
+    has(loci: StructureElement.Loci) {
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return false;
+        return StructureElement.Loci.areIntersecting(loci, entry.selection);
+    }
+
+    tryGetRange(loci: StructureElement.Loci): StructureElement.Loci | undefined {
+        if (loci.elements.length !== 1) return;
+        const entry = this.getEntry(loci.structure);
+        if (!entry) return;
+
+        let xs = loci.elements[0];
+        let e: StructureElement.Loci['elements'][0] | undefined;
+        for (const _e of entry.selection.elements) {
+            if (xs.unit === _e.unit) {
+                e = _e;
+                break;
+            }
+        }
+        if (!e) return;
+
+        let predIdx = OrderedSet.findPredecessorIndex(e.indices, OrderedSet.min(xs.indices));
+        if (predIdx === 0) return;
+
+        let fst;
+
+        if (predIdx < OrderedSet.size(e.indices)) {
+            fst = OrderedSet.getAt(e.indices, predIdx)
+            if (fst > OrderedSet.min(xs.indices)) fst = OrderedSet.getAt(e.indices, predIdx - 1) + 1 as StructureElement.UnitIndex;
+        } else {
+            fst = OrderedSet.getAt(e.indices, predIdx - 1) + 1 as StructureElement.UnitIndex;
+        }
+
+        return StructureElement.Loci(entry.selection.structure, [{
+            unit: e.unit,
+            indices: OrderedSet.ofRange(fst, OrderedSet.max(xs.indices))
+        }]);
+    }
+
+    private prevHighlight: StructureElement.Loci | undefined = void 0;
+
+    accumulateInteractiveHighlight(loci: StructureElement.Loci) {
+        if (this.prevHighlight) {
+            this.prevHighlight = StructureElement.Loci.union(this.prevHighlight, loci);
+        } else {
+            this.prevHighlight = loci;
+        }
+        return this.prevHighlight;
+    }
+
+    clearInteractiveHighlight() {
+        const ret = this.prevHighlight;
+        this.prevHighlight = void 0;
+        return ret || EmptyLoci;
+    }
+
+    private onRemove(ref: string) {
+        if (this.entries.has(ref)) this.entries.delete(ref);
+    }
+
+    private onUpdate(ref: string, oldObj: StateObject | undefined, obj: StateObject) {
+        if (!PluginStateObject.Molecule.Structure.is(obj)) return;
+
+        if (this.entries.has(ref)) {
+            if (!PluginStateObject.Molecule.Structure.is(oldObj) || oldObj === obj || oldObj.data === obj.data) return;
+
+            // remap the old selection to be related to the new object if possible.
+            if (Structure.areUnitAndIndicesEqual(oldObj.data, obj.data)) {
+                this.entries.set(ref, remapSelectionEntry(this.entries.get(ref)!, obj.data));
+                return;
+            }
+
+            // clear the selection
+            this.entries.set(ref, SelectionEntry(obj.data));
+        }
+    }
+
+    constructor(private plugin: PluginContext) {
+        plugin.state.dataState.events.object.removed.subscribe(e => this.onRemove(e.ref));
+        plugin.state.dataState.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
+    }
+}
+
+interface SelectionEntry {
+    selection: StructureElement.Loci
+}
+
+function SelectionEntry(s: Structure): SelectionEntry {
+    return {
+        selection: StructureElement.Loci(s, [])
+    };
+}
+
+function remapSelectionEntry(e: SelectionEntry, s: Structure): SelectionEntry {
+    return {
+        selection: StructureElement.Loci.remap(e.selection, s)
+    };
+}
\ No newline at end of file
diff --git a/src/mol-plugin/util/structure-labels.ts b/src/mol-plugin/util/structure-labels.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c6e944b29219ac96fed56f47556ad3885780ea36
--- /dev/null
+++ b/src/mol-plugin/util/structure-labels.ts
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure, StructureElement, StructureProperties, Unit } from 'mol-model/structure';
+import { StateTransformer } from 'mol-state';
+import { StructureLabels3D } from '../state/transforms/representation';
+import { ShapeRepresentation } from 'mol-repr/shape/representation';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Text } from 'mol-geo/geometry/text/text';
+import { TextBuilder } from 'mol-geo/geometry/text/text-builder';
+import { Shape } from 'mol-model/shape';
+import { ColorNames } from 'mol-util/color/tables';
+import { RuntimeContext } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { BoundaryHelper } from 'mol-math/geometry/boundary-helper';
+
+interface LabelsData {
+    texts: string[],
+    positions: Vec3[],
+    sizes: number[],
+    depths: number[]
+}
+
+function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: Text) {
+    const { texts, positions, depths } = data
+    const textBuilder = TextBuilder.create(props, texts.length * 10, texts.length * 10 / 2, text)
+    for (let i = 0, il = texts.length; i < il; ++i) {
+        const p = positions[i]
+        textBuilder.add(texts[i], p[0], p[1], p[2], depths[i], i)
+    }
+    return textBuilder.getText()
+}
+
+export async function getLabelRepresentation(ctx: RuntimeContext, structure: Structure, params: StateTransformer.Params<StructureLabels3D>, prev?: ShapeRepresentation<LabelsData, Text, Text.Params>) {
+    const repr = prev || ShapeRepresentation(getLabelsShape, Text.Utils);
+    const data = getLabelData(structure, params);
+    await repr.createOrUpdate(params.options, data).runInContext(ctx);
+    repr.setState({ pickable: false })
+    return repr;
+}
+
+function getLabelsShape(ctx: RuntimeContext, data: LabelsData, props: PD.Values<Text.Params>, shape?: Shape<Text>) {
+    const geo = getLabelsText(data, props, shape && shape.geometry);
+    return Shape.create('Scene Labels', geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '')
+}
+
+const boundaryHelper = new BoundaryHelper();
+function getLabelData(structure: Structure, params: StateTransformer.Params<StructureLabels3D>): LabelsData {
+    if (params.target.name === 'static-text') {
+        return getLabelDataStatic(structure, params.target.params.value, params.target.params.size || 1, params.target.params.position || 'middle-center');
+    } else {
+        return getLabelDataComputed(structure, params.target.name);
+    }
+
+}
+
+function getLabelDataStatic(structure: Structure, text: string, size: number, position: Text.Params['attachment']['defaultValue']): LabelsData {
+    const boundary = structure.boundary.sphere;
+    let oX = 0, oY = 0;
+    if (position.indexOf('left') >= 0) oX = -boundary.radius;
+    if (position.indexOf('right') >= 0) oX = boundary.radius;
+    if (position.indexOf('top') >= 0) oY = boundary.radius;
+    if (position.indexOf('bottom') >= 0) oY = -boundary.radius;
+    return {
+        texts: [text],
+        positions: [Vec3.add(Vec3.zero(), boundary.center, Vec3.create(oX, oY, 0))],
+        sizes: [size],
+        depths: [boundary.radius + Math.sqrt(oX * oX + oY * oY)]
+    };
+}
+
+function getLabelDataComputed(structure: Structure, level: 'elements' | 'residues'): LabelsData {
+    const data: LabelsData = { texts: [], positions: [], sizes: [], depths: [] };
+
+    const l = StructureElement.create();
+    const { units } = structure;
+
+    const { auth_atom_id } = StructureProperties.atom;
+    const { auth_seq_id, auth_comp_id } = StructureProperties.residue;
+    const { auth_asym_id } = StructureProperties.chain;
+    const p = Vec3.zero();
+
+    for (const unit of units) {
+        // TODO: support coarse models
+
+        if (unit.kind !== Unit.Kind.Atomic) continue;
+        l.unit = unit;
+        const elements = unit.elements;
+
+        const pos = unit.conformation.position;
+
+        if (level === 'elements') {
+            for (let j = 0, _j = elements.length; j < _j; j++) {
+                l.element = elements[j];
+
+                pos(l.element, p);
+                data.texts.push(auth_atom_id(l));
+                data.positions.push(Vec3.clone(p));
+                data.sizes.push(1);
+                data.depths.push(2);
+            }
+        } else {
+            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index;
+
+            let i = 0, len = elements.length;
+            while (i < len) {
+                const start = i, rI = residueIndex[elements[i]];
+                i++;
+                while (i < len && residueIndex[elements[i]] === rI) i++;
+
+                boundaryHelper.reset(0);
+                for (let eI = start; eI < i; eI++) {
+                    pos(elements[eI], p);
+                    boundaryHelper.boundaryStep(p, 0);
+                }
+                boundaryHelper.finishBoundaryStep();
+                for (let eI = start; eI < i; eI++) {
+                    pos(elements[eI], p);
+                    boundaryHelper.extendStep(p, 0);
+                }
+
+                l.element = elements[start];
+
+                data.texts.push(`${auth_comp_id(l)} ${auth_seq_id(l)}:${auth_asym_id(l)}`);
+                data.positions.push(Vec3.clone(boundaryHelper.center));
+                data.sizes.push(Math.max(1, boundaryHelper.radius / 5));
+                data.depths.push(boundaryHelper.radius);
+            }
+        }
+    }
+
+    return data;
+}
\ No newline at end of file
diff --git a/src/mol-plugin/util/substructure-parent-helper.ts b/src/mol-plugin/util/substructure-parent-helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..322f43f73fc08805df92ca02e2017e43d5f308de
--- /dev/null
+++ b/src/mol-plugin/util/substructure-parent-helper.ts
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { State, StateObject, StateSelection, StateObjectCell } from 'mol-state';
+import { PluginContext } from '../context';
+import { PluginStateObject } from '../state/objects';
+
+export { SubstructureParentHelper };
+
+class SubstructureParentHelper {
+    private root = new Map<Structure, string>();
+    private tracked = new Map<string, Structure>();
+
+    get(s: Structure): StateObjectCell<PluginStateObject.Molecule.Structure> | undefined {
+        const r = this.root.get(s);
+        if (!r) return;
+        return this.plugin.state.dataState.cells.get(r);
+    }
+
+    private addMapping(state: State, ref: string, obj: StateObject) {
+        if (!PluginStateObject.Molecule.Structure.is(obj)) return;
+        const parent = state.select(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+        this.tracked.set(ref, obj.data);
+        if (!parent) {
+            this.root.set(obj.data, ref);
+        } else {
+            this.root.set(obj.data, parent.transform.ref);
+        }
+    }
+
+    private removeMapping(ref: string) {
+        if (!this.tracked.has(ref)) return;
+        const s = this.tracked.get(ref)!;
+        this.tracked.delete(ref);
+        this.root.get(s);
+        this.root.delete(s);
+    }
+
+    private updateMapping(state: State, ref: string, oldObj: StateObject | undefined, obj: StateObject) {
+        if (!PluginStateObject.Molecule.Structure.is(obj)) return;
+
+        this.removeMapping(ref);
+        this.addMapping(state, ref, obj);
+    }
+
+    constructor(private plugin: PluginContext) {
+        plugin.state.dataState.events.object.created.subscribe(e => {
+            this.addMapping(e.state, e.ref, e.obj);
+        });
+
+        plugin.state.dataState.events.object.removed.subscribe(e => {
+            this.removeMapping(e.ref);
+        });
+
+        plugin.state.dataState.events.object.updated.subscribe(e => {
+            this.updateMapping(e.state, e.ref, e.oldObj, e.obj);
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/mol-plugin/util/task-manager.ts b/src/mol-plugin/util/task-manager.ts
index 3c5cbb47e72aa442378ceae3bdd8df06d1193a30..d053c4a618e29b5c8c93b05210d44c8d6d002c06 100644
--- a/src/mol-plugin/util/task-manager.ts
+++ b/src/mol-plugin/util/task-manager.ts
@@ -13,18 +13,21 @@ export { TaskManager }
 class TaskManager {
     private ev = RxEventHelper.create();
     private id = 0;
+    private abortRequests = new Map<number, string | undefined>();
 
     readonly events = {
         progress: this.ev<TaskManager.ProgressEvent>(),
         finished: this.ev<{ id: number }>()
     };
 
-    private track(id: number) {
+    private track(internalId: number, taskId: number) {
         return (progress: Progress) => {
+            if (progress.canAbort && progress.requestAbort && this.abortRequests.has(taskId)) {
+                progress.requestAbort(this.abortRequests.get(taskId));
+            }
             const elapsed = now() - progress.root.progress.startedTime;
-            progress.root.progress.startedTime
             this.events.progress.next({
-                id,
+                id: internalId,
                 level: elapsed < 250 ? 'none' : elapsed < 1500 ? 'background' : 'overlay',
                 progress
             });
@@ -34,13 +37,18 @@ class TaskManager {
     async run<T>(task: Task<T>): Promise<T> {
         const id = this.id++;
         try {
-            const ret = await task.run(this.track(id), 100);
+            const ret = await task.run(this.track(id, task.id), 100);
             return ret;
         } finally {
             this.events.finished.next({ id });
+            this.abortRequests.delete(task.id);
         }
     }
 
+    requestAbort(task: Task<any> | number, reason?: string) {
+        this.abortRequests.set(typeof task === 'number' ? task : task.id, reason);
+    }
+
     dispose() {
         this.ev.dispose();
     }
diff --git a/src/mol-plugin/version.ts b/src/mol-plugin/version.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fe3cfb827da23a0837aceacd75f69a5faf90c779
--- /dev/null
+++ b/src/mol-plugin/version.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const PLUGIN_VERSION = '0.5.0';
+/** unix time stamp, to be filled in at bundle build time */
+declare const __PLUGIN_VERSION_TIMESTAMP__: number
+export const PLUGIN_VERSION_TIMESTAMP = __PLUGIN_VERSION_TIMESTAMP__;
+export const PLUGIN_VERSION_DATE = new Date(PLUGIN_VERSION_TIMESTAMP);
\ No newline at end of file
diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts
index 41369fd550e27e223c926e648a694b1b0ea42a9f..5bc851e2fdbac8163d79b00a4fdd8a5c18b319b9 100644
--- a/src/mol-repr/representation.ts
+++ b/src/mol-repr/representation.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,7 +7,7 @@
 import { Task } from 'mol-task'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
 import { PickingId } from '../mol-geo/geometry/picking';
-import { Loci, isEmptyLoci, EmptyLoci } from 'mol-model/loci';
+import { Loci as ModelLoci, isEmptyLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../mol-geo/geometry/marker-data';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { WebGLContext } from 'mol-gl/webgl/context';
@@ -18,6 +18,8 @@ import { Theme, ThemeRegistryContext, createEmptyTheme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { BaseGeometry } from 'mol-geo/geometry/base';
+import { Visual } from './visual';
+import { Overpaint } from 'mol-theme/overpaint';
 
 // export interface RepresentationProps {
 //     visuals?: string[]
@@ -31,21 +33,29 @@ export interface RepresentationContext {
 }
 
 export type RepresentationParamsGetter<D, P extends PD.Params> = (ctx: ThemeRegistryContext, data: D) => P
-export type RepresentationFactory<D, P extends PD.Params> = (ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>) => Representation<D, P>
+export type RepresentationFactory<D, P extends PD.Params, S extends Representation.State> = (ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>) => Representation<D, P, S>
 
 //
 
-export interface RepresentationProvider<D, P extends PD.Params> {
+export interface RepresentationProvider<D, P extends PD.Params, S extends Representation.State> {
     readonly label: string
     readonly description: string
-    readonly factory: RepresentationFactory<D, P>
+    readonly factory: RepresentationFactory<D, P, S>
     readonly getParams: RepresentationParamsGetter<D, P>
     readonly defaultValues: PD.Values<P>
     readonly defaultColorTheme: string
     readonly defaultSizeTheme: string
 }
 
-export type AnyRepresentationProvider = RepresentationProvider<any, {}>
+export namespace RepresentationProvider {
+    export type ParamValues<R extends RepresentationProvider<any, any, any>> = R extends RepresentationProvider<any, infer P, any> ? PD.Values<P> : never;
+
+    export function getDetaultParams<R extends RepresentationProvider<D, any, any>, D>(r: R, ctx: ThemeRegistryContext, data: D) {
+        return PD.getDefaultValues(r.getParams(ctx, data));
+    }
+}
+
+export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State>
 
 export const EmptyRepresentationProvider = {
     label: '',
@@ -55,9 +65,10 @@ export const EmptyRepresentationProvider = {
     defaultValues: {}
 }
 
-export class RepresentationRegistry<D> {
-    private _list: { name: string, provider: RepresentationProvider<D, any> }[] = []
-    private _map = new Map<string, RepresentationProvider<D, any>>()
+export class RepresentationRegistry<D, S extends Representation.State> {
+    private _list: { name: string, provider: RepresentationProvider<D, any, any> }[] = []
+    private _map = new Map<string, RepresentationProvider<D, any, any>>()
+    private _name = new Map<RepresentationProvider<D, any, any>, string>()
 
     get default() { return this._list[0]; }
     get types(): [string, string][] {
@@ -66,18 +77,28 @@ export class RepresentationRegistry<D> {
 
     constructor() {};
 
-    add<P extends PD.Params>(name: string, provider: RepresentationProvider<D, P>) {
+    add<P extends PD.Params>(name: string, provider: RepresentationProvider<D, P, S>) {
         this._list.push({ name, provider })
         this._map.set(name, provider)
+        this._name.set(provider, name)
+    }
+
+    getName(provider: RepresentationProvider<D, any, S>): string {
+        if (!this._name.has(provider)) throw new Error(`'${provider.label}' is not a registered represenatation provider.`);
+        return this._name.get(provider)!;
     }
 
     remove(name: string) {
         this._list.splice(this._list.findIndex(e => e.name === name), 1)
-        this._map.delete(name)
+        const p = this._map.get(name);
+        if (p) {
+            this._map.delete(name);
+            this._name.delete(p);
+        }
     }
 
-    get<P extends PD.Params>(name: string): RepresentationProvider<D, P> {
-        return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P>
+    get<P extends PD.Params>(name: string): RepresentationProvider<D, P, S> {
+        return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P, S>
     }
 
     get list() {
@@ -88,7 +109,7 @@ export class RepresentationRegistry<D> {
 //
 
 export { Representation }
-interface Representation<D, P extends PD.Params = {}> {
+interface Representation<D, P extends PD.Params = {}, S extends Representation.State = Representation.State> {
     readonly label: string
     readonly updated: Subject<number>
     /** Number of addressable groups in all visuals of the representation */
@@ -96,37 +117,58 @@ interface Representation<D, P extends PD.Params = {}> {
     readonly renderObjects: ReadonlyArray<GraphicsRenderObject>
     readonly props: Readonly<PD.Values<P>>
     readonly params: Readonly<P>
-    readonly state: Readonly<Representation.State>
+    readonly state: Readonly<S>
     readonly theme: Readonly<Theme>
     createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
-    setState: (state: Partial<Representation.State>) => void
+    setState: (state: Partial<S>) => void
     setTheme: (theme: Theme) => void
-    getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => boolean
+    getLoci: (pickingId: PickingId) => ModelLoci
+    mark: (loci: ModelLoci, action: MarkerAction) => boolean
     destroy: () => void
 }
 namespace Representation {
+    export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
+
+    export namespace Loci {
+        export function areEqual(a: Loci, b: Loci) {
+            return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
+        }
+
+        export const Empty: Loci = { loci: EmptyLoci };
+    }
+
     export interface State {
         /** Controls if the representation's renderobjects are rendered or not */
         visible: boolean
+        /** A factor applied to alpha value of the representation's renderobjects */
+        alphaFactor: number
         /** Controls if the representation's renderobjects are pickable or not */
         pickable: boolean
+        /** Overpaint applied to the representation's renderobjects */
+        overpaint: Overpaint
         /** Controls if the representation's renderobjects are synced automatically with GPU or not */
         syncManually: boolean
         /** A transformation applied to the representation's renderobjects */
         transform: Mat4
     }
-    export function createState() {
-        return { visible: false, pickable: false, syncManually: false, transform: Mat4.identity() }
+    export function createState(): State {
+        return { visible: false, alphaFactor: 0, pickable: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty }
     }
     export function updateState(state: State, update: Partial<State>) {
         if (update.visible !== undefined) state.visible = update.visible
+        if (update.alphaFactor !== undefined) state.alphaFactor = update.alphaFactor
         if (update.pickable !== undefined) state.pickable = update.pickable
+        if (update.overpaint !== undefined) state.overpaint = update.overpaint
         if (update.syncManually !== undefined) state.syncManually = update.syncManually
         if (update.transform !== undefined) Mat4.copy(state.transform, update.transform)
     }
+    export interface StateBuilder<S extends State> {
+        create(): S
+        update(state: S, update: Partial<S>): void
+    }
+    export const StateBuilder: StateBuilder<State> = { create: createState, update: updateState }
 
-    export type Any = Representation<any, any>
+    export type Any = Representation<any, any, any>
     export const Empty: Any = {
         label: '', groupCount: 0, renderObjects: [], props: {}, params: {}, updated: new Subject(), state: createState(), theme: createEmptyTheme(),
         createOrUpdate: () => Task.constant('', undefined),
@@ -137,12 +179,12 @@ namespace Representation {
         destroy: () => {}
     }
 
-    export type Def<D, P extends PD.Params = {}> = { [k: string]: RepresentationFactory<D, P> }
+    export type Def<D, P extends PD.Params = {}, S extends State = State> = { [k: string]: RepresentationFactory<D, P, S> }
 
-    export function createMulti<D, P extends PD.Params = {}>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> {
+    export function createMulti<D, P extends PD.Params = {}, S extends State = State>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>, stateBuilder: StateBuilder<S>, reprDefs: Def<D, P>): Representation<D, P, S> {
         let version = 0
         const updated = new Subject<number>()
-        const currentState = Representation.createState()
+        const currentState = stateBuilder.create()
         let currentTheme = createEmptyTheme()
 
         let currentParams: P
@@ -182,11 +224,7 @@ namespace Representation {
                 }
                 return renderObjects
             },
-            get props() {
-                const props = {}
-                reprList.forEach(r => Object.assign(props, r.props))
-                return props as P
-            },
+            get props() { return currentProps },
             get params() { return currentParams },
             createOrUpdate: (props: Partial<P> = {}, data?: D) => {
                 if (data && data !== currentData) {
@@ -216,18 +254,18 @@ namespace Representation {
                 }
                 return EmptyLoci
             },
-            mark: (loci: Loci, action: MarkerAction) => {
+            mark: (loci: ModelLoci, action: MarkerAction) => {
                 let marked = false
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     marked = reprList[i].mark(loci, action) || marked
                 }
                 return marked
             },
-            setState: (state: Partial<State>) => {
+            setState: (state: Partial<S>) => {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     reprList[i].setState(state)
                 }
-                Representation.updateState(currentState, state)
+                stateBuilder.update(currentState, state)
             },
             setTheme: (theme: Theme) => {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
@@ -273,14 +311,18 @@ namespace Representation {
                 // TODO
                 return EmptyLoci
             },
-            mark: (loci: Loci, action: MarkerAction) => {
+            mark: (loci: ModelLoci, action: MarkerAction) => {
                 // TODO
                 return false
             },
             setState: (state: Partial<State>) => {
-                if (state.visible !== undefined) renderObject.state.visible = state.visible
-                if (state.pickable !== undefined) renderObject.state.pickable = state.pickable
-                // TODO transform
+                if (state.visible !== undefined) Visual.setVisibility(renderObject, state.visible)
+                if (state.alphaFactor !== undefined) Visual.setAlphaFactor(renderObject, state.alphaFactor)
+                if (state.pickable !== undefined) Visual.setPickable(renderObject, state.pickable)
+                if (state.overpaint !== undefined) {
+                    // TODO
+                }
+                if (state.transform !== undefined) Visual.setTransform(renderObject, state.transform)
 
                 Representation.updateState(currentState, state)
             },
diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts
index d0cec8c695b11ae764bce218f07d7e4c6c2de871..83f1edb96e25a5ed817d4cba918fb1d2a9ec803b 100644
--- a/src/mol-repr/shape/representation.ts
+++ b/src/mol-repr/shape/representation.ts
@@ -14,7 +14,7 @@ import { OrderedSet, Interval } from 'mol-data/int';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { createTransform, TransformData } from 'mol-geo/geometry/transform-data';
 import { PickingId } from 'mol-geo/geometry/picking';
-import { MarkerAction, applyMarkerAction, createMarkers } from 'mol-geo/geometry/marker-data';
+import { MarkerAction, createMarkers } from 'mol-geo/geometry/marker-data';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { createEmptyTheme, Theme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
@@ -95,14 +95,14 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             }
 
             if (updateState.createNew) {
-                renderObjects.length = 0
+                renderObjects.length = 0 // clear list o renderObjects
                 locationIt = ShapeGroupIterator.fromShape(_shape)
                 const transform = createShapeTransform(_shape.transforms)
                 const values = geometryUtils.createValues(_shape.geometry, transform, locationIt, _theme, newProps)
                 const state = geometryUtils.createRenderableState(newProps)
 
                 _renderObject = createRenderObject(_shape.geometry.kind, values, state)
-                if (_renderObject) renderObjects.push(_renderObject)
+                if (_renderObject) renderObjects.push(_renderObject) // add new renderObject to list
             } else {
                 if (!_renderObject) {
                     throw new Error('expected renderObject to be available')
@@ -144,18 +144,27 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             }
 
             currentProps = newProps
+            // increment version
             updated.next(version++)
         });
     }
 
+    function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
+        if (isEveryLoci(loci) || (Shape.isLoci(loci) && loci.shape === _shape)) {
+            return apply(Interval.ofBounds(0, _shape.groupCount * _shape.transforms.length))
+        } else {
+            return eachShapeGroup(loci, _shape, apply)
+        }
+    }
+
     return {
         label: 'Shape geometry',
         get groupCount () { return locationIt ? locationIt.count : 0 },
-        get renderObjects () { return renderObjects },
         get props () { return currentProps },
         get params () { return currentParams },
         get state() { return _state },
         get theme() { return _theme },
+        renderObjects,
         updated,
         createOrUpdate,
         getLoci(pickingId: PickingId) {
@@ -166,36 +175,16 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
-            if (!_renderObject) return false
-            const { tMarker } = _renderObject.values
-            let changed = false
-            const { groupCount, count } = locationIt
-            if (isEveryLoci(loci)) {
-                if (applyMarkerAction(tMarker.ref.value.array, 0, count, action)) changed = true
-            } else if (Shape.isLoci(loci)) {
-                const { instance, groups } = loci
-                for (const g of groups) {
-                    if (Interval.is(g.ids)) {
-                        const start = instance * groupCount + Interval.start(g.ids)
-                        const end = instance * groupCount + Interval.end(g.ids)
-                        if (applyMarkerAction(tMarker.ref.value.array, start, end, action)) changed = true
-                    } else {
-                        for (let i = 0, _i = g.ids.length; i < _i; i++) {
-                            const idx = instance * groupCount + g.ids[i];
-                            if (applyMarkerAction(tMarker.ref.value.array, idx, idx + 1, action)) changed = true
-                        }
-                    }
-                }
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
+            return Visual.mark(_renderObject, loci, action, lociApply)
         },
         setState(state: Partial<Representation.State>) {
             if (_renderObject) {
                 if (state.visible !== undefined) Visual.setVisibility(_renderObject, state.visible)
+                if (state.alphaFactor !== undefined) Visual.setAlphaFactor(_renderObject, state.alphaFactor)
                 if (state.pickable !== undefined) Visual.setPickable(_renderObject, state.pickable)
+                if (state.overpaint !== undefined) {
+                    Visual.setOverpaint(_renderObject, state.overpaint, lociApply, true)
+                }
                 if (state.transform !== undefined) Visual.setTransform(_renderObject, state.transform)
             }
 
@@ -220,6 +209,27 @@ function createShapeTransform(transforms: Mat4[], transformData?: TransformData)
     return createTransform(transformArray, transforms.length, transformData)
 }
 
+function eachShapeGroup(loci: Loci, shape: Shape, apply: (interval: Interval) => boolean) {
+    if (!Shape.isLoci(loci)) return false
+    if (loci.shape !== shape) return false
+    let changed = false
+    const { groupCount } = shape
+    const { instance, groups } = loci
+    for (const g of groups) {
+        if (Interval.is(g.ids)) {
+            const start = instance * groupCount + Interval.start(g.ids)
+            const end = instance * groupCount + Interval.end(g.ids)
+            if (apply(Interval.ofBounds(start, end))) changed = true
+        } else {
+            for (let i = 0, _i = g.ids.length; i < _i; i++) {
+                const idx = instance * groupCount + g.ids[i];
+                if (apply(Interval.ofSingleton(idx))) changed = true
+            }
+        }
+    }
+    return changed
+}
+
 export namespace ShapeGroupIterator {
     export function fromShape(shape: Shape): LocationIterator {
         const instanceCount = shape.transforms.length
diff --git a/src/mol-repr/structure/complex-representation.ts b/src/mol-repr/structure/complex-representation.ts
index 187f8608d23642d29bff26e0871a600f7be124e1..bbb8325859e481f190099c5e212bb79588ce0b95 100644
--- a/src/mol-repr/structure/complex-representation.ts
+++ b/src/mol-repr/structure/complex-representation.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>
@@ -8,19 +8,21 @@
 import { Structure } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { Loci, EmptyLoci } from 'mol-model/loci';
-import { StructureRepresentation, StructureParams } from './representation';
+import { StructureRepresentation, StructureParams, StructureRepresentationStateBuilder, StructureRepresentationState } from './representation';
 import { ComplexVisual } from './complex-visual';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { RepresentationContext, RepresentationParamsGetter, Representation } from 'mol-repr/representation';
+import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
+import { GraphicsRenderObject } from 'mol-gl/render-object';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
-    const _state = Representation.createState()
+    const renderObjects: GraphicsRenderObject[] = []
+    const _state = StructureRepresentationStateBuilder.create()
     let visual: ComplexVisual<P> | undefined
 
     let _structure: Structure
@@ -40,6 +42,10 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
             if (!visual) visual = visualCtor()
             const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure)
             if (promise) await promise
+            // update list of renderObjects
+            renderObjects.length = 0
+            if (visual && visual.renderObject) renderObjects.push(visual.renderObject)
+            // increment version
             updated.next(version++)
         });
     }
@@ -52,12 +58,22 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         return visual ? visual.mark(loci, action) : false
     }
 
-    function setState(state: Partial<Representation.State>) {
-        if (state.visible !== undefined && visual) visual.setVisibility(state.visible)
+    function setState(state: Partial<StructureRepresentationState>) {
+        StructureRepresentationStateBuilder.update(_state, state)
+
+        if (state.visible !== undefined && visual) {
+            // hide visual when _unitTransforms is set
+            visual.setVisibility(state.visible && _state.unitTransforms === null)
+        }
+        if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor)
         if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
+        if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint)
         if (state.transform !== undefined && visual) visual.setTransform(state.transform)
-
-        Representation.updateState(_state, state)
+        if (state.unitTransforms !== undefined && visual) {
+            // Since ComplexVisuals always renders geometries between units the application of `unitTransforms`
+            // does not make sense. When given it is ignored here and sets the visual's visibility to `false`.
+            visual.setVisibility(_state.visible && state.unitTransforms === null)
+        }
     }
 
     function setTheme(theme: Theme) {
@@ -73,13 +89,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         get groupCount() {
             return visual ? visual.groupCount : 0
         },
-        get renderObjects() {
-            return visual && visual.renderObject ? [ visual.renderObject ] : []
-        },
         get props() { return _props },
         get params() { return _params },
         get state() { return _state },
         get theme() { return _theme },
+        renderObjects,
         updated,
         createOrUpdate,
         setState,
diff --git a/src/mol-repr/structure/complex-visual.ts b/src/mol-repr/structure/complex-visual.ts
index d84f2c495d1908d80445ce12b0775250afb9306c..7f5407789c126d221f45642205eab9576b0150df 100644
--- a/src/mol-repr/structure/complex-visual.ts
+++ b/src/mol-repr/structure/complex-visual.ts
@@ -18,7 +18,7 @@ import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { createColors } from 'mol-geo/geometry/color-data';
-import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
+import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { VisualUpdateState } from 'mol-repr/util';
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
@@ -28,6 +28,7 @@ import { UnitsParams } from './units-representation';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
+import { Overpaint } from 'mol-theme/overpaint';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
 
@@ -50,7 +51,7 @@ interface ComplexVisualBuilder<P extends ComplexParams, G extends Geometry> {
     createGeometry(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
     createLocationIterator(structure: Structure): LocationIterator
     getLoci(pickingId: PickingId, structure: Structure, id: number): Loci
-    mark(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean): boolean,
+    eachLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean): boolean,
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
@@ -59,7 +60,7 @@ interface ComplexVisualGeometryBuilder<P extends UnitsParams, G extends Geometry
 }
 
 export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>): ComplexVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
     const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
 
@@ -157,6 +158,14 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom
         if (newGeometry) geometry = newGeometry
     }
 
+    function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
+        if (isEveryLoci(loci) || (Structure.isLoci(loci) && Structure.areEquivalent(loci.structure, currentStructure))) {
+            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount))
+        } else {
+            return eachLocation(loci, currentStructure, apply)
+        }
+    }
+
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
         get renderObject () { return locationIt && locationIt.count ? renderObject : undefined },
@@ -173,35 +182,22 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom
             return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructure)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, currentStructure, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
+            return Visual.mark(renderObject, loci, action, lociApply)
+        },
+        setVisibility(visible: boolean) {
+            Visual.setVisibility(renderObject, visible)
+        },
+        setAlphaFactor(alphaFactor: number) {
+            Visual.setAlphaFactor(renderObject, alphaFactor)
         },
-        setVisibility(value: boolean) {
-            Visual.setVisibility(renderObject, value)
+        setPickable(pickable: boolean) {
+            Visual.setPickable(renderObject, pickable)
         },
-        setPickable(value: boolean) {
-            Visual.setPickable(renderObject, value)
+        setTransform(matrix?: Mat4, instanceMatrices?: Float32Array | null) {
+            Visual.setTransform(renderObject, matrix, instanceMatrices)
         },
-        setTransform(value: Mat4) {
-            Visual.setTransform(renderObject, value)
+        setOverpaint(overpaint: Overpaint, clear = false) {
+            return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
         destroy() {
             // TODO
diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts
index 12802514cc0dcf6719dc1a206d04786853f33207..5cadc109a7933a576c8e291c75c97d052765a476 100644
--- a/src/mol-repr/structure/registry.ts
+++ b/src/mol-repr/structure/registry.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>
  */
@@ -14,12 +14,14 @@ import { CarbohydrateRepresentationProvider } from './representation/carbohydrat
 import { SpacefillRepresentationProvider } from './representation/spacefill';
 import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint';
 import { PointRepresentationProvider } from './representation/point';
+import { StructureRepresentationState } from './representation';
+import { PuttyRepresentationProvider } from './representation/putty';
 
-export class StructureRepresentationRegistry extends RepresentationRegistry<Structure> {
+export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> {
     constructor() {
         super()
         Object.keys(BuiltInStructureRepresentations).forEach(name => {
-            const p = (BuiltInStructureRepresentations as { [k: string]: RepresentationProvider<Structure, any> })[name]
+            const p = (BuiltInStructureRepresentations as { [k: string]: RepresentationProvider<Structure, any, StructureRepresentationState> })[name]
             this.add(name, p)
         })
     }
@@ -33,6 +35,7 @@ export const BuiltInStructureRepresentations = {
     'molecular-surface': MolecularSurfaceRepresentationProvider,
     'molecular-volume': MolecularVolumeRepresentationProvider,
     'point': PointRepresentationProvider,
+    'putty': PuttyRepresentationProvider,
     'spacefill': SpacefillRepresentationProvider,
 }
 export type BuiltInStructureRepresentationsName = keyof typeof BuiltInStructureRepresentations
diff --git a/src/mol-repr/structure/representation.ts b/src/mol-repr/structure/representation.ts
index 6e2991d13f4973d5f922e355c22fbeebb92b85e6..183cd297c381b744e1abfe7a073d28b9322fefb4 100644
--- a/src/mol-repr/structure/representation.ts
+++ b/src/mol-repr/structure/representation.ts
@@ -14,10 +14,27 @@ import { Points } from 'mol-geo/geometry/points/points';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { Spheres } from 'mol-geo/geometry/spheres/spheres';
-
-export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { }
-
-export type StructureRepresentationProvider<P extends PD.Params> = RepresentationProvider<Structure, P>
+import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
+
+export interface StructureRepresentationState extends Representation.State {
+    unitTransforms: StructureUnitTransforms | null
+}
+export const StructureRepresentationStateBuilder: Representation.StateBuilder<StructureRepresentationState> = {
+    create: () => {
+        return {
+            ...Representation.createState(),
+            unitTransforms: null
+        }
+    },
+    update: (state: StructureRepresentationState, update: Partial<StructureRepresentationState>) => {
+        Representation.updateState(state, update)
+        if (update.unitTransforms !== undefined) state.unitTransforms = update.unitTransforms
+    }
+}
+
+export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P, StructureRepresentationState> { }
+
+export type StructureRepresentationProvider<P extends PD.Params> = RepresentationProvider<Structure, P, StructureRepresentationState>
 
 //
 
diff --git a/src/mol-repr/structure/representation/ball-and-stick.ts b/src/mol-repr/structure/representation/ball-and-stick.ts
index 0146c906126da9efed2b514cc62eb0e70f0a0dec..a7b3e9accf7d1a73008a0a5caad0fc46c748eb36 100644
--- a/src/mol-repr/structure/representation/ball-and-stick.ts
+++ b/src/mol-repr/structure/representation/ball-and-stick.ts
@@ -10,7 +10,7 @@ import { InterUnitLinkVisual, InterUnitLinkParams } from '../visual/inter-unit-l
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
 import { ComplexRepresentation } from '../complex-representation';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -40,7 +40,7 @@ export function getBallAndStickParams(ctx: ThemeRegistryContext, structure: Stru
 
 export type BallAndStickRepresentation = StructureRepresentation<BallAndStickParams>
 export function BallAndStickRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BallAndStickParams>): BallAndStickRepresentation {
-    return Representation.createMulti('Ball & Stick', ctx, getParams, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>)
+    return Representation.createMulti('Ball & Stick', ctx, getParams, StructureRepresentationStateBuilder, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>)
 }
 
 export const BallAndStickRepresentationProvider: StructureRepresentationProvider<BallAndStickParams> = {
diff --git a/src/mol-repr/structure/representation/carbohydrate.ts b/src/mol-repr/structure/representation/carbohydrate.ts
index a71144a064cea93fddf73db62966e0d252821844..a4f4d3079ce4b63ce9356e4e7322d28d354e4ede 100644
--- a/src/mol-repr/structure/representation/carbohydrate.ts
+++ b/src/mol-repr/structure/representation/carbohydrate.ts
@@ -9,7 +9,7 @@ import { CarbohydrateLinkVisual, CarbohydrateLinkParams } from '../visual/carboh
 import { CarbohydrateTerminalLinkParams, CarbohydrateTerminalLinkVisual } from '../visual/carbohydrate-terminal-link-cylinder';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ComplexRepresentation } from '../complex-representation';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -35,7 +35,7 @@ export function getCarbohydrateParams(ctx: ThemeRegistryContext, structure: Stru
 
 export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateParams>
 export function CarbohydrateRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateParams>): CarbohydrateRepresentation {
-    return Representation.createMulti('Carbohydrate', ctx, getParams, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>)
+    return Representation.createMulti('Carbohydrate', ctx, getParams, StructureRepresentationStateBuilder, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>)
 }
 
 export const CarbohydrateRepresentationProvider: StructureRepresentationProvider<CarbohydrateParams> = {
diff --git a/src/mol-repr/structure/representation/cartoon.ts b/src/mol-repr/structure/representation/cartoon.ts
index 5dfed8882e77416a1582696660bb8c93b5681426..1fef6f9ef60ba99e322b3acc418ebfc9577c9434 100644
--- a/src/mol-repr/structure/representation/cartoon.ts
+++ b/src/mol-repr/structure/representation/cartoon.ts
@@ -9,16 +9,18 @@ import { PolymerGapVisual, PolymerGapParams } from '../visual/polymer-gap-cylind
 import { NucleotideBlockVisual, NucleotideBlockParams } from '../visual/nucleotide-block-mesh';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { PolymerDirectionVisual, PolymerDirectionParams } from '../visual/polymer-direction-wedge';
 import { Structure, Unit } from 'mol-model/structure';
 import { ThemeRegistryContext } from 'mol-theme/theme';
+import { NucleotideRingParams, NucleotideRingVisual } from '../visual/nucleotide-ring-mesh';
 
 const CartoonVisuals = {
     'polymer-trace': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', ctx, getParams, PolymerTraceVisual),
     'polymer-gap': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', ctx, getParams, PolymerGapVisual),
     'nucleotide-block': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NucleotideBlockParams>) => UnitsRepresentation('Nucleotide block mesh', ctx, getParams, NucleotideBlockVisual),
+    'nucleotide-ring': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NucleotideRingParams>) => UnitsRepresentation('Nucleotide ring mesh', ctx, getParams, NucleotideRingVisual),
     'direction-wedge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerDirectionParams>) => UnitsRepresentation('Polymer direction wedge', ctx, getParams, PolymerDirectionVisual)
 }
 type CartoonVisualName = keyof typeof CartoonVisuals
@@ -28,6 +30,7 @@ export const CartoonParams = {
     ...PolymerTraceParams,
     ...PolymerGapParams,
     ...NucleotideBlockParams,
+    ...NucleotideRingParams,
     ...PolymerDirectionParams,
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
     visuals: PD.MultiSelect<CartoonVisualName>(['polymer-trace', 'polymer-gap', 'nucleotide-block'], CartoonVisualOptions),
@@ -49,12 +52,12 @@ export function getCartoonParams(ctx: ThemeRegistryContext, structure: Structure
 
 export type CartoonRepresentation = StructureRepresentation<CartoonParams>
 export function CartoonRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CartoonParams>): CartoonRepresentation {
-    return Representation.createMulti('Cartoon', ctx, getParams, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>)
+    return Representation.createMulti('Cartoon', ctx, getParams, StructureRepresentationStateBuilder, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>)
 }
 
 export const CartoonRepresentationProvider: StructureRepresentationProvider<CartoonParams> = {
     label: 'Cartoon',
-    description: 'Displays a ribbon smoothly following the trace atoms of polymers.',
+    description: 'Displays ribbons, planks, tubes smoothly following the trace atoms of polymers.',
     factory: CartoonRepresentation,
     getParams: getCartoonParams,
     defaultValues: PD.getDefaultValues(CartoonParams),
diff --git a/src/mol-repr/structure/representation/distance-restraint.ts b/src/mol-repr/structure/representation/distance-restraint.ts
index ca1c8a8819531d2fef7b73b8c6073620787a3d6e..096bf268b829982f631008c7f1bde095d4e6a48f 100644
--- a/src/mol-repr/structure/representation/distance-restraint.ts
+++ b/src/mol-repr/structure/representation/distance-restraint.ts
@@ -7,7 +7,7 @@
 import { CrossLinkRestraintVisual, CrossLinkRestraintParams } from '../visual/cross-link-restraint-cylinder';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ComplexRepresentation } from '../complex-representation';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -28,7 +28,7 @@ export function getDistanceRestraintParams(ctx: ThemeRegistryContext, structure:
 
 export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintParams>
 export function DistanceRestraintRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, DistanceRestraintParams>): DistanceRestraintRepresentation {
-    return Representation.createMulti('DistanceRestraint', ctx, getParams, DistanceRestraintVisuals as unknown as Representation.Def<Structure, DistanceRestraintParams>)
+    return Representation.createMulti('DistanceRestraint', ctx, getParams, StructureRepresentationStateBuilder, DistanceRestraintVisuals as unknown as Representation.Def<Structure, DistanceRestraintParams>)
 }
 
 export const DistanceRestraintRepresentationProvider: StructureRepresentationProvider<DistanceRestraintParams> = {
diff --git a/src/mol-repr/structure/representation/molecular-surface.ts b/src/mol-repr/structure/representation/molecular-surface.ts
index 48d5b9f48c34d038f18873f547cdbf145d33540b..4cef6716dca74fcef9b2ec284b41b1b41eaee9f3 100644
--- a/src/mol-repr/structure/representation/molecular-surface.ts
+++ b/src/mol-repr/structure/representation/molecular-surface.ts
@@ -8,7 +8,7 @@ import { GaussianSurfaceVisual, GaussianSurfaceParams } from '../visual/gaussian
 import { UnitsRepresentation } from '../units-representation';
 import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -32,7 +32,7 @@ export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure:
 
 export type MolecularSurfaceRepresentation = StructureRepresentation<MolecularSurfaceParams>
 export function MolecularSurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularSurfaceParams>): MolecularSurfaceRepresentation {
-    return Representation.createMulti('Molecular Surface', ctx, getParams, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>)
+    return Representation.createMulti('Molecular Surface', ctx, getParams, StructureRepresentationStateBuilder, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>)
 }
 
 export const MolecularSurfaceRepresentationProvider: StructureRepresentationProvider<MolecularSurfaceParams> = {
diff --git a/src/mol-repr/structure/representation/molecular-volume.ts b/src/mol-repr/structure/representation/molecular-volume.ts
index d437203678c75fb13e21d67130e7d6d6ce153fc9..4bf0a0141629d5912bd6cf5de50e001a4ed3a0da 100644
--- a/src/mol-repr/structure/representation/molecular-volume.ts
+++ b/src/mol-repr/structure/representation/molecular-volume.ts
@@ -6,7 +6,7 @@
 
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { GaussianDensityVolumeParams, GaussianDensityVolumeVisual } from '../visual/gaussian-density-volume';
-import { StructureRepresentation, StructureRepresentationProvider, ComplexRepresentation } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, ComplexRepresentation, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -25,7 +25,7 @@ export function getMolecularVolumeParams(ctx: ThemeRegistryContext, structure: S
 
 export type MolecularVolumeRepresentation = StructureRepresentation<MolecularVolumeParams>
 export function MolecularVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularVolumeParams>): MolecularVolumeRepresentation {
-    return Representation.createMulti('Molecular Volume', ctx, getParams, MolecularVolumeVisuals as unknown as Representation.Def<Structure, MolecularVolumeParams>)
+    return Representation.createMulti('Molecular Volume', ctx, getParams, StructureRepresentationStateBuilder, MolecularVolumeVisuals as unknown as Representation.Def<Structure, MolecularVolumeParams>)
 }
 
 export const MolecularVolumeRepresentationProvider: StructureRepresentationProvider<MolecularVolumeParams> = {
diff --git a/src/mol-repr/structure/representation/point.ts b/src/mol-repr/structure/representation/point.ts
index 9a1789271d384a46dc9af306591e487ef3562be0..fe54c0e2c1c417e6fbfdc48403bbc7a6a5f3662d 100644
--- a/src/mol-repr/structure/representation/point.ts
+++ b/src/mol-repr/structure/representation/point.ts
@@ -7,7 +7,7 @@
 import { ElementPointVisual, ElementPointParams } from '../visual/element-point';
 import { UnitsRepresentation } from '../units-representation';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -28,7 +28,7 @@ export function getPointParams(ctx: ThemeRegistryContext, structure: Structure)
 
 export type PointRepresentation = StructureRepresentation<PointParams>
 export function PointRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PointParams>): PointRepresentation {
-    return Representation.createMulti('Point', ctx, getParams, PointVisuals as unknown as Representation.Def<Structure, PointParams>)
+    return Representation.createMulti('Point', ctx, getParams, StructureRepresentationStateBuilder, PointVisuals as unknown as Representation.Def<Structure, PointParams>)
 }
 
 export const PointRepresentationProvider: StructureRepresentationProvider<PointParams> = {
diff --git a/src/mol-repr/structure/representation/putty.ts b/src/mol-repr/structure/representation/putty.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b2d7f9c98cd8afbfea14fdd2145df50a84b1ae90
--- /dev/null
+++ b/src/mol-repr/structure/representation/putty.ts
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PolymerTubeVisual,  PolymerTubeParams } from '../visual/polymer-tube-mesh';
+import { PolymerGapVisual, PolymerGapParams } from '../visual/polymer-gap-cylinder';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { UnitsRepresentation } from '../units-representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
+import { Structure, Unit } from 'mol-model/structure';
+import { ThemeRegistryContext } from 'mol-theme/theme';
+
+const PuttyVisuals = {
+    'polymer-tube': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTubeParams>) => UnitsRepresentation('Polymer tube mesh', ctx, getParams, PolymerTubeVisual),
+    'polymer-gap': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', ctx, getParams, PolymerGapVisual),
+}
+type PuttyVisualName = keyof typeof PuttyVisuals
+const PuttyVisualOptions = Object.keys(PuttyVisuals).map(name => [name, name] as [PuttyVisualName, string])
+
+export const PuttyParams = {
+    ...PolymerTubeParams,
+    ...PolymerGapParams,
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    visuals: PD.MultiSelect<PuttyVisualName>(['polymer-tube', 'polymer-gap'], PuttyVisualOptions),
+}
+export type PuttyParams = typeof PuttyParams
+export function getPuttyParams(ctx: ThemeRegistryContext, structure: Structure) {
+    const params = PD.clone(PuttyParams)
+    let hasNucleotides = false
+    let hasGaps = false
+    structure.units.forEach(u => {
+        if (!hasNucleotides && Unit.isAtomic(u) && u.nucleotideElements.length) hasNucleotides = true
+        if (!hasGaps && u.gapElements.length) hasGaps = true
+    })
+    params.visuals.defaultValue = ['polymer-tube']
+    if (hasGaps) params.visuals.defaultValue.push('polymer-gap')
+    return params
+}
+
+export type PuttyRepresentation = StructureRepresentation<PuttyParams>
+export function PuttyRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PuttyParams>): PuttyRepresentation {
+    return Representation.createMulti('Putty', ctx, getParams, StructureRepresentationStateBuilder, PuttyVisuals as unknown as Representation.Def<Structure, PuttyParams>)
+}
+
+export const PuttyRepresentationProvider: StructureRepresentationProvider<PuttyParams> = {
+    label: 'Putty',
+    description: 'Displays a tube smoothly following the trace atoms of polymers.',
+    factory: PuttyRepresentation,
+    getParams: getPuttyParams,
+    defaultValues: PD.getDefaultValues(PuttyParams),
+    defaultColorTheme: 'polymer-id',
+    defaultSizeTheme: 'uncertainty'
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/representation/spacefill.ts b/src/mol-repr/structure/representation/spacefill.ts
index ad310ca55c54e09d6e747ba231115701c83eaf36..74ca4264ac9695dc178f65830c6816532261103e 100644
--- a/src/mol-repr/structure/representation/spacefill.ts
+++ b/src/mol-repr/structure/representation/spacefill.ts
@@ -7,7 +7,7 @@
 import { getElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
 import { UnitsRepresentation } from '../units-representation';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
+import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
 import { RepresentationParamsGetter, RepresentationContext, Representation } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
@@ -28,7 +28,7 @@ export function getSpacefillParams(ctx: ThemeRegistryContext, structure: Structu
 
 export type SpacefillRepresentation = StructureRepresentation<SpacefillParams>
 export function SpacefillRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, SpacefillParams>): SpacefillRepresentation {
-    return Representation.createMulti('Spacefill', ctx, getParams, SpacefillVisuals as unknown as Representation.Def<Structure, SpacefillParams>)
+    return Representation.createMulti('Spacefill', ctx, getParams, StructureRepresentationStateBuilder, SpacefillVisuals as unknown as Representation.Def<Structure, SpacefillParams>)
 }
 
 export const SpacefillRepresentationProvider: StructureRepresentationProvider<SpacefillParams> = {
diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts
index d5a3d64761dc34f58045351b07a2db673ac5e7f2..92e0c6e28cb1581a2aa9843a2eb2921898ebaa19 100644
--- a/src/mol-repr/structure/units-representation.ts
+++ b/src/mol-repr/structure/units-representation.ts
@@ -8,11 +8,11 @@
 import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { GraphicsRenderObject } from 'mol-gl/render-object';
-import { RepresentationContext, RepresentationParamsGetter, Representation } from '../representation';
+import { RepresentationContext, RepresentationParamsGetter } from '../representation';
 import { Visual } from '../visual';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
 import { StructureGroup } from './units-visual';
-import { StructureRepresentation, StructureParams } from './representation';
+import { StructureRepresentation, StructureParams, StructureRepresentationState, StructureRepresentationStateBuilder } from './representation';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
@@ -31,7 +31,8 @@ export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGrou
 export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
-    const _state = Representation.createState()
+    const renderObjects: GraphicsRenderObject[] = []
+    const _state = StructureRepresentationStateBuilder.create()
     let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
 
     let _structure: Structure
@@ -140,7 +141,14 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: il })
                 }
             }
+            // update list of renderObjects
+            renderObjects.length = 0
+            visuals.forEach(({ visual }) => {
+                if (visual.renderObject) renderObjects.push(visual.renderObject)
+            })
+            // set new structure
             if (structure) _structure = structure
+            // increment version
             updated.next(version++)
         });
     }
@@ -162,12 +170,25 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
         return changed
     }
 
-    function setState(state: Partial<Representation.State>) {
-        if (state.visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(state.visible!))
-        if (state.pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(state.pickable!))
-        if (state.transform !== undefined) visuals.forEach(({ visual }) => visual.setTransform(state.transform!))
+    function setState(state: Partial<StructureRepresentationState>) {
+        const { visible, alphaFactor, pickable, overpaint, transform, unitTransforms } = state
+        if (visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(visible))
+        if (alphaFactor !== undefined) visuals.forEach(({ visual }) => visual.setAlphaFactor(alphaFactor))
+        if (pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(pickable))
+        if (overpaint !== undefined) visuals.forEach(({ visual }) => visual.setOverpaint(overpaint))
+        if (transform !== undefined) visuals.forEach(({ visual }) => visual.setTransform(transform))
+        if (unitTransforms !== undefined) {
+            visuals.forEach(({ visual, group }) => {
+                if (unitTransforms) {
+                    // console.log(group.hashCode, unitTransforms.getSymmetryGroupTransforms(group))
+                    visual.setTransform(undefined, unitTransforms.getSymmetryGroupTransforms(group))
+                } else {
+                    visual.setTransform(undefined, null)
+                }
+            })
+        }
 
-        Representation.updateState(_state, state)
+        StructureRepresentationStateBuilder.update(_state, state)
     }
 
     function setTheme(theme: Theme) {
@@ -188,17 +209,11 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R
             })
             return groupCount
         },
-        get renderObjects() {
-            const renderObjects: GraphicsRenderObject[] = []
-            visuals.forEach(({ visual }) => {
-                if (visual.renderObject) renderObjects.push(visual.renderObject)
-            })
-            return renderObjects
-        },
         get props() { return _props },
         get params() { return _params },
         get state() { return _state },
         get theme() { return _theme },
+        renderObjects,
         updated,
         createOrUpdate,
         setState,
diff --git a/src/mol-repr/structure/units-visual.ts b/src/mol-repr/structure/units-visual.ts
index 94454569810f767c8d12643313b1011b77ee3282..4f3c5bf22635d6cd2a7c1a1f09d8d7e5059241f9 100644
--- a/src/mol-repr/structure/units-visual.ts
+++ b/src/mol-repr/structure/units-visual.ts
@@ -16,7 +16,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { PickingId } from 'mol-geo/geometry/picking';
-import { createMarkers, MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
+import { createMarkers, MarkerAction } from 'mol-geo/geometry/marker-data';
 import { createSizes } from 'mol-geo/geometry/size-data';
 import { createColors } from 'mol-geo/geometry/color-data';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
@@ -31,6 +31,7 @@ import { UnitsParams } from './units-representation';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { Spheres } from 'mol-geo/geometry/spheres/spheres';
 import { createUnitsTransform, includesUnitKind } from './visual/util/common';
+import { Overpaint } from 'mol-theme/overpaint';
 
 export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
 
@@ -49,7 +50,7 @@ interface UnitsVisualBuilder<P extends UnitsParams, G extends Geometry> {
     createGeometry(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
     createLocationIterator(group: Unit.SymmetryGroup): LocationIterator
     getLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number): Loci
-    mark(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean): boolean
+    eachLocation(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean): boolean
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
@@ -58,7 +59,7 @@ interface UnitsVisualGeometryBuilder<P extends UnitsParams, G extends Geometry>
 }
 
 export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
     const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
 
@@ -88,7 +89,7 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
 
         if (!renderObject) {
             updateState.createNew = true
-        } else if (!currentStructureGroup || newStructureGroup.group.hashCode !== currentStructureGroup.group.hashCode) {
+        } else if (!currentStructureGroup || !Unit.SymmetryGroup.areInvariantElementsEqual(newStructureGroup.group, currentStructureGroup.group)) {
             updateState.createNew = true
         }
 
@@ -203,6 +204,14 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
                 : createEmptyGeometry(geometry)
     }
 
+    function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
+        if (isEveryLoci(loci) || (Structure.isLoci(loci) && Structure.areEquivalent(loci.structure, currentStructureGroup.structure))) {
+            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount))
+        } else {
+            return eachLocation(loci, currentStructureGroup, apply)
+        }
+    }
+
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
         get renderObject () { return locationIt && locationIt.count ? renderObject : undefined },
@@ -219,35 +228,22 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry
             return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructureGroup.structure)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, currentStructureGroup, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
+            return Visual.mark(renderObject, loci, action, lociApply)
+        },
+        setVisibility(visible: boolean) {
+            Visual.setVisibility(renderObject, visible)
+        },
+        setAlphaFactor(alphaFactor: number) {
+            Visual.setAlphaFactor(renderObject, alphaFactor)
         },
-        setVisibility(value: boolean) {
-            Visual.setVisibility(renderObject, value)
+        setPickable(pickable: boolean) {
+            Visual.setPickable(renderObject, pickable)
         },
-        setPickable(value: boolean) {
-            Visual.setPickable(renderObject, value)
+        setTransform(matrix?: Mat4, instanceMatrices?: Float32Array | null) {
+            Visual.setTransform(renderObject, matrix, instanceMatrices)
         },
-        setTransform(value: Mat4) {
-            Visual.setTransform(renderObject, value)
+        setOverpaint(overpaint: Overpaint) {
+            return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
         destroy() {
             // TODO
diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
index e9c9cfb4aecff654415755b60b5240fe371c39d1..c699d7464aecdc28c8c48e764db74c516e48d872 100644
--- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts
@@ -61,7 +61,7 @@ export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams>
         createGeometry: createCarbohydrateLinkCylinderMesh,
         createLocationIterator: CarbohydrateLinkIterator,
         getLoci: getLinkLoci,
-        mark: markLink,
+        eachLocation: eachCarbohydrateLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateLinkParams>, currentProps: PD.Values<CarbohydrateLinkParams>) => {
             state.createGeometry = (
                 newProps.linkSizeFactor !== currentProps.linkSizeFactor ||
@@ -116,10 +116,10 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     return EmptyLoci
 }
 
-function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+function eachCarbohydrateLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
     if (Link.isLoci(loci)) {
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         const { getLinkIndex } = structure.carbohydrates
         for (const l of loci.links) {
             const idx = getLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
@@ -128,7 +128,7 @@ function markLink(loci: Loci, structure: Structure, apply: (interval: Interval)
             }
         }
     } else if (StructureElement.isLoci(loci)) {
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         const { getElementIndex, getLinkIndices, elements } = structure.carbohydrates
         for (const e of loci.elements) {
diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
index e0df24ab33d18aed5bf4c6ae9f823ff656618be8..bc4e4ad6f71267d9e7599562be026c6324863535 100644
--- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
@@ -154,7 +154,7 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolPara
         createGeometry: createCarbohydrateSymbolMesh,
         createLocationIterator: CarbohydrateElementIterator,
         getLoci: getCarbohydrateLoci,
-        mark: markCarbohydrate,
+        eachLocation: eachCarbohydrate,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateSymbolParams>, currentProps: PD.Values<CarbohydrateSymbolParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
@@ -191,29 +191,29 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     return EmptyLoci
 }
 
-/** Mark a carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
-function markCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+/** For each carbohydrate (usually a monosaccharide) when all its residue's elements are in a loci. */
+function eachCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const { getElementIndex, getAnomericCarbon } = structure.carbohydrates
     let changed = false
-    if (StructureElement.isLoci(loci)) {
-        for (const e of loci.elements) {
-            OrderedSet.forEach(e.indices, v => {
-                const { model, elements } = e.unit
-                const { index, offsets } = model.atomicHierarchy.residueAtomSegments
-                const rI = index[elements[v]]
-                const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
-                const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
-                const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-                const eI = getAnomericCarbon(e.unit, rI)
-                if (eI !== undefined) {
-                    const idx = getElementIndex(e.unit, eI)
-                    if (idx !== undefined) {
-                        if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true
-                    }
+    if (!StructureElement.isLoci(loci)) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
+    for (const e of loci.elements) {
+        OrderedSet.forEach(e.indices, v => {
+            const { model, elements } = e.unit
+            const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+            const rI = index[elements[v]]
+            const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
+            const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
+            const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
+            if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+            const eI = getAnomericCarbon(e.unit, rI)
+            if (eI !== undefined) {
+                const idx = getElementIndex(e.unit, eI)
+                if (idx !== undefined) {
+                    if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true
                 }
-            })
-        }
+            }
+        })
     }
     return changed
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
index 69368d4ca87dc326cf110bb2a7e7ec7fc9bceaa9..d78b03bd16a475cbfc7aa617f9d77a27104ef1d9 100644
--- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
@@ -71,7 +71,7 @@ export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerm
         createGeometry: createCarbohydrateTerminalLinkCylinderMesh,
         createLocationIterator: CarbohydrateTerminalLinkIterator,
         getLoci: getTerminalLinkLoci,
-        mark: markTerminalLink,
+        eachLocation: eachTerminalLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateTerminalLinkParams>, currentProps: PD.Values<CarbohydrateTerminalLinkParams>) => {
             state.createGeometry = (
                 newProps.linkSizeFactor !== currentProps.linkSizeFactor ||
@@ -128,12 +128,12 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num
     return EmptyLoci
 }
 
-// TODO mark link when both of the link elements are in a StructureElement.Loci
-function markTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+// TODO for each link when both of the link elements are in a StructureElement.Loci
+function eachTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const { getTerminalLinkIndex } = structure.carbohydrates
-
     let changed = false
     if (Link.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         for (const l of loci.links) {
             const idx = getTerminalLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
             if (idx !== undefined) {
@@ -141,7 +141,7 @@ function markTerminalLink(loci: Loci, structure: Structure, apply: (interval: In
             }
         }
     } else if (StructureElement.isLoci(loci)) {
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         const { getElementIndex, getTerminalLinkIndices, elements } = structure.carbohydrates
         for (const e of loci.elements) {
diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
index 5c5c8d388a3417dcb0d09687c6f3481d24325664..26de0839cfa9744b7ecd4e5e980ec98afc4da141 100644
--- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
+++ b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts
@@ -64,7 +64,7 @@ export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintPara
         createGeometry: createCrossLinkRestraintCylinderMesh,
         createLocationIterator: CrossLinkRestraintIterator,
         getLoci: getLinkLoci,
-        mark: markLink,
+        eachLocation: eachCrossLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CrossLinkRestraintParams>, currentProps: PD.Values<CrossLinkRestraintParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
@@ -104,11 +104,11 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     return EmptyLoci
 }
 
-function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+function eachCrossLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const crossLinks = structure.crossLinkRestraints
-
     let changed = false
     if (Link.isLoci(loci)) {
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         for (const b of loci.links) {
             const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
             if (indices) {
diff --git a/src/mol-repr/structure/visual/element-point.ts b/src/mol-repr/structure/visual/element-point.ts
index 73c77715bc4a31d5039ea78476d1d42aa5b57f22..64db953cbb4402571c10e3db99a2cf44d8109da8 100644
--- a/src/mol-repr/structure/visual/element-point.ts
+++ b/src/mol-repr/structure/visual/element-point.ts
@@ -7,7 +7,7 @@
 import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { getElementLoci, StructureElementIterator, markElement } from './util/element';
+import { getElementLoci, StructureElementIterator, eachElement } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsPointsVisual, UnitsPointsParams } from '../units-visual';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -48,7 +48,7 @@ export function ElementPointVisual(): UnitsVisual<ElementPointParams> {
         createGeometry: createElementPoint,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementPointParams>, currentProps: PD.Values<ElementPointParams>) => {
 
         }
diff --git a/src/mol-repr/structure/visual/element-sphere.ts b/src/mol-repr/structure/visual/element-sphere.ts
index 4608121fc85792e2a9036afd7339f6774fb64491..afbe90fbb144fe2a3dad2aff1817781fa19df137 100644
--- a/src/mol-repr/structure/visual/element-sphere.ts
+++ b/src/mol-repr/structure/visual/element-sphere.ts
@@ -7,7 +7,7 @@
 
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { createElementSphereMesh, markElement, getElementLoci, StructureElementIterator, createElementSphereImpostor } from './util/element';
+import { createElementSphereMesh, eachElement, getElementLoci, StructureElementIterator, createElementSphereImpostor } from './util/element';
 import { UnitsMeshVisual, UnitsMeshParams, UnitsSpheresVisual, UnitsSpheresParams } from '../units-visual';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { WebGLContext } from 'mol-gl/webgl/context';
@@ -30,7 +30,7 @@ export function ElementSphereImpostorVisual(): UnitsVisual<ElementSphereParams>
         createGeometry: createElementSphereImpostor,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementSphereParams>, currentProps: PD.Values<ElementSphereParams>) => {
 
         }
@@ -43,7 +43,7 @@ export function ElementSphereMeshVisual(): UnitsVisual<ElementSphereParams> {
         createGeometry: createElementSphereMesh,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementSphereParams>, currentProps: PD.Values<ElementSphereParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
diff --git a/src/mol-repr/structure/visual/gaussian-density-point.ts b/src/mol-repr/structure/visual/gaussian-density-point.ts
index 0be7361e871d576a0ba60281b6afd55178834149..beef6aae62dbbf963c6d2b9460309e24664d48d4 100644
--- a/src/mol-repr/structure/visual/gaussian-density-point.ts
+++ b/src/mol-repr/structure/visual/gaussian-density-point.ts
@@ -11,12 +11,12 @@ import { StructureElementIterator } from './util/element';
 import { EmptyLoci } from 'mol-model/loci';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsPointsVisual, UnitsPointsParams } from '../units-visual';
-import { GaussianDensityProps, GaussianDensityParams } from 'mol-model/structure/structure/unit/gaussian-density';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Points } from 'mol-geo/geometry/points/points';
 import { PointsBuilder } from 'mol-geo/geometry/points/points-builder';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
+import { computeUnitGaussianDensity, GaussianDensityParams, GaussianDensityProps } from './util/gaussian';
 
 export const GaussianDensityPointParams = {
     ...UnitsPointsParams,
@@ -26,7 +26,7 @@ export const GaussianDensityPointParams = {
 export type GaussianDensityPointParams = typeof GaussianDensityPointParams
 
 export async function createGaussianDensityPoint(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, points?: Points) {
-    const { transform, field: { space, data } } = await unit.computeGaussianDensity(props, ctx.runtime, ctx.webgl)
+    const { transform, field: { space, data } } = await computeUnitGaussianDensity(unit, props, ctx.webgl).runInContext(ctx.runtime)
 
     const { dimensions, get } = space
     const [ xn, yn, zn ] = dimensions
@@ -61,13 +61,12 @@ export function GaussianDensityPointVisual(): UnitsVisual<GaussianDensityPointPa
         createGeometry: createGaussianDensityPoint,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: () => EmptyLoci,
-        mark: () => false,
+        eachLocation: () => false,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianDensityPointParams>, currentProps: PD.Values<GaussianDensityPointParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
             if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
-            if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
         }
     })
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-density-volume.ts b/src/mol-repr/structure/visual/gaussian-density-volume.ts
index e5a8a9683e593169e342375e4cd66090c5360119..7aac5a7b9aafbbe775f4769c0051f77c36e00152 100644
--- a/src/mol-repr/structure/visual/gaussian-density-volume.ts
+++ b/src/mol-repr/structure/visual/gaussian-density-volume.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Structure, StructureElement } from 'mol-model/structure';
+import { Structure } from 'mol-model/structure';
 import { VisualUpdateState } from '../../util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
@@ -14,56 +14,9 @@ import { ComplexVisual, ComplexDirectVolumeVisual, ComplexDirectVolumeParams } f
 import { EmptyLoci } from 'mol-model/loci';
 import { NullLocation } from 'mol-model/location';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { WebGLContext } from 'mol-gl/webgl/context';
-import { Texture } from 'mol-gl/webgl/texture';
-import { GaussianDensityTexture } from 'mol-math/geometry/gaussian-density/gpu';
-import { Task } from 'mol-task';
-import { OrderedSet } from 'mol-data/int';
-import { PhysicalSizeTheme } from 'mol-theme/size/physical';
+import { computeStructureGaussianDensityTexture, GaussianDensityTextureProps, GaussianDensityTextureParams } from './util/gaussian';
 
-function getConformationAndRadius(structure: Structure) {
-    const n = structure.elementCount
-
-    const xs = new Float32Array(n)
-    const ys = new Float32Array(n)
-    const zs = new Float32Array(n)
-    const rs = new Float32Array(n)
-
-    const l = StructureElement.create()
-    const sizeTheme = PhysicalSizeTheme({}, {})
-
-    let m = 0
-    for (let i = 0, il = structure.units.length; i < il; ++i) {
-        const unit = structure.units[i]
-        const { elements } = unit
-        const { x, y, z } = unit.conformation
-        l.unit = unit
-        for (let j = 0, jl = elements.length; j < jl; ++j) {
-            const eI = elements[j]
-            xs[m + j] = x(eI)
-            ys[m + j] = y(eI)
-            zs[m + j] = z(eI)
-            l.element = eI
-            rs[m + j] = sizeTheme.size(l)
-        }
-        m += elements.length
-    }
-
-    const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs }
-    const radius = (index: number) => rs[index]
-
-    return { position, radius }
-}
-
-// TODO calculate by combining unit volumes
-function computeStructureGaussianDensityTexture(structure: Structure, props: PD.Values<GaussianDensityVolumeParams>, webgl: WebGLContext, texture?: Texture) {
-    const { position, radius } = getConformationAndRadius(structure)
-    return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensityTexture(ctx, webgl, position, structure.lookup3d.boundary.box, radius, props, texture);
-    });
-}
-
-async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<GaussianDensityVolumeParams>, directVolume?: DirectVolume): Promise<DirectVolume> {
+async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx
     if (webgl === undefined) throw new Error('createGaussianDensityVolume requires `webgl` object in VisualContext')
 
@@ -77,9 +30,7 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
 
 export const GaussianDensityVolumeParams = {
     ...ComplexDirectVolumeParams,
-    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
-    radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
-    smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
+    ...GaussianDensityTextureParams
 }
 export type GaussianDensityVolumeParams = typeof GaussianDensityVolumeParams
 
@@ -89,13 +40,13 @@ export function GaussianDensityVolumeVisual(): ComplexVisual<GaussianDensityVolu
         createGeometry: createGaussianDensityVolume,
         createLocationIterator: (structure: Structure) => LocationIterator(structure.elementCount, 1, () => NullLocation),
         getLoci: () => EmptyLoci, // TODO
-        mark: () => false, // TODO
+        eachLocation: () => false, // TODO
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianDensityVolumeParams>, currentProps: PD.Values<GaussianDensityVolumeParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
             if (newProps.smoothness !== currentProps.smoothness) {
                 state.createGeometry = true
-                newProps.isoValue = Math.exp(-newProps.smoothness)
+                newProps.isoValueNorm = Math.exp(-newProps.smoothness)
             }
         }
     })
diff --git a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
index 0c01c3c0f6a0aa65bfefc1cca37f8b17edd1565c..166252a81a076d7e1ccd496890c2c63496264b8b 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
@@ -8,17 +8,17 @@ import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
-import { StructureElementIterator, getElementLoci, markElement } from './util/element';
-import { GaussianDensityProps, GaussianDensityParams } from 'mol-model/structure/structure/unit/gaussian-density';
+import { StructureElementIterator, getElementLoci, eachElement } from './util/element';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
+import { GaussianDensityProps, computeUnitGaussianDensity, GaussianDensityParams } from './util/gaussian';
 
 async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props
-    const { transform, field, idField } = await unit.computeGaussianDensity(props, ctx.runtime, ctx.webgl)
+    const { transform, field, idField } = await computeUnitGaussianDensity(unit, props, ctx.webgl).runInContext(ctx.runtime)
 
     const params = {
         isoLevel: Math.exp(-smoothness),
@@ -46,13 +46,12 @@ export function GaussianSurfaceVisual(): UnitsVisual<GaussianSurfaceParams> {
         createGeometry: createGaussianSurfaceMesh,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceParams>, currentProps: PD.Values<GaussianSurfaceParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
             if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
-            if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
         }
     })
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
index e17ebb6e265aea176828c3fadc037889595e73c7..5efe3f47bf6395431987a0717b27e6f7d4558b38 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
@@ -8,17 +8,17 @@ import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
 import { UnitsLinesVisual, UnitsLinesParams } from '../units-visual';
-import { StructureElementIterator, getElementLoci, markElement } from './util/element';
-import { GaussianDensityProps, GaussianDensityParams } from 'mol-model/structure/structure/unit/gaussian-density';
+import { StructureElementIterator, getElementLoci, eachElement } from './util/element';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 import { computeMarchingCubesLines } from 'mol-geo/util/marching-cubes/algorithm';
 import { VisualContext } from 'mol-repr/visual';
 import { Theme } from 'mol-theme/theme';
+import { GaussianDensityProps, GaussianDensityParams, computeUnitGaussianDensity } from './util/gaussian';
 
 async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, lines?: Lines): Promise<Lines> {
     const { smoothness } = props
-    const { transform, field, idField } = await unit.computeGaussianDensity(props, ctx.runtime)
+    const { transform, field, idField } = await computeUnitGaussianDensity(unit, props, ctx.webgl).runInContext(ctx.runtime)
 
     const params = {
         isoLevel: Math.exp(-smoothness),
@@ -45,13 +45,12 @@ export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeParams>
         createGeometry: createGaussianWireframe,
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianWireframeParams>, currentProps: PD.Values<GaussianWireframeParams>) => {
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true
             if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true
-            if (newProps.ignoreCache !== currentProps.ignoreCache) state.createGeometry = true
         }
     })
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
index b16a312d3ef7f29ccd74a2a61242ac4cac050307..fa55745425b88e8b15149d9d2e83ee4af92e5377 100644
--- a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts
@@ -64,7 +64,7 @@ export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> {
         createGeometry: createInterUnitLinkCylinderMesh,
         createLocationIterator: LinkIterator.fromStructure,
         getLoci: getLinkLoci,
-        mark: markLink,
+        eachLocation: eachLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitLinkParams>, currentProps: PD.Values<InterUnitLinkParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
@@ -95,10 +95,10 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     return EmptyLoci
 }
 
-function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+function eachLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
     if (Link.isLoci(loci)) {
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         for (const b of loci.links) {
             const idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
             if (idx !== -1) {
@@ -106,7 +106,7 @@ function markLink(loci: Loci, structure: Structure, apply: (interval: Interval)
             }
         }
     } else if (StructureElement.isLoci(loci)) {
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         // TODO mark link only when both of the link elements are in a StructureElement.Loci
         for (const e of loci.elements) {
             OrderedSet.forEach(e.indices, v => {
diff --git a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
index fad35df529db12baacb5b1ac74b2c5b74788e6ff..9f86c6700696e789bcfd4b4b4895b981facc26bc 100644
--- a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
+++ b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts
@@ -40,12 +40,18 @@ function createIntraUnitLinkCylinderMesh(ctx: VisualContext, unit: Unit, structu
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
             let aI = a[edgeIndex], bI = b[edgeIndex];
+
             if (aI > bI) [aI, bI] = [bI, aI]
+            if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI]
+            // TODO prefer reference atoms in rings
+
             for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
-                if (b[i] !== bI) return pos(elements[b[i]], vRef)
+                const _bI = b[i]
+                if (_bI !== bI && _bI !== aI) return pos(elements[_bI], vRef)
             }
             for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
-                if (a[i] !== aI) return pos(elements[a[i]], vRef)
+                const _aI = a[i]
+                if (_aI !== aI && _aI !== bI) return pos(elements[_aI], vRef)
             }
             return null
         },
@@ -78,7 +84,7 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> {
         createGeometry: createIntraUnitLinkCylinderMesh,
         createLocationIterator: LinkIterator.fromGroup,
         getLoci: getLinkLoci,
-        mark: markLink,
+        eachLocation: eachLink,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitLinkParams>, currentProps: PD.Values<IntraUnitLinkParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
@@ -112,11 +118,11 @@ function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: n
     return EmptyLoci
 }
 
-function markLink(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+function eachLink(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (Link.isLoci(loci)) {
         const { structure, group } = structureGroup
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         const unit = group.units[0]
         if (!Unit.isAtomic(unit)) return false
         const groupCount = unit.links.edgeCount * 2
@@ -131,7 +137,7 @@ function markLink(loci: Loci, structureGroup: StructureGroup, apply: (interval:
         }
     } else if (StructureElement.isLoci(loci)) {
         const { structure, group } = structureGroup
-        if (loci.structure !== structure) return false
+        if (!Structure.areEquivalent(loci.structure, structure)) return false
         const unit = group.units[0]
         if (!Unit.isAtomic(unit)) return false
         const groupCount = unit.links.edgeCount * 2
diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
index 6e638cf3ed2f6a5ead2fe6dbf404408a013c73a3..78fa5aa2f974b63e88cac56235da5aa3a09a66d4 100644
--- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
+++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts
@@ -10,7 +10,7 @@ import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { Segmentation } from 'mol-data/int';
 import { isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
-import { NucleotideLocationIterator, markNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
+import { NucleotideLocationIterator, eachNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Box } from 'mol-geo/primitive/box';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
@@ -134,7 +134,7 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> {
         createGeometry: createNucleotideBlockMesh,
         createLocationIterator: NucleotideLocationIterator.fromGroup,
         getLoci: getNucleotideElementLoci,
-        mark: markNucleotideElement,
+        eachLocation: eachNucleotideElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NucleotideBlockParams>, currentProps: PD.Values<NucleotideBlockParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6baef02237f105ec1392ceb42465fa160525e571
--- /dev/null
+++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Structure, ElementIndex } from 'mol-model/structure';
+import { UnitsVisual } from '../representation';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Segmentation } from 'mol-data/int';
+import { isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
+import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
+import { NucleotideLocationIterator, eachNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
+import { addCylinder } from 'mol-geo/geometry/mesh/builder/cylinder';
+import { VisualContext } from 'mol-repr/visual';
+import { Theme } from 'mol-theme/theme';
+import { VisualUpdateState } from 'mol-repr/util';
+import { CylinderProps } from 'mol-geo/primitive/cylinder';
+import { NumberArray } from 'mol-util/type-helpers';
+import { addSphere } from 'mol-geo/geometry/mesh/builder/sphere';
+
+const pTrace = Vec3.zero()
+const pN1 = Vec3.zero()
+const pC2 = Vec3.zero()
+const pN3 = Vec3.zero()
+const pC4 = Vec3.zero()
+const pC5 = Vec3.zero()
+const pC6 = Vec3.zero()
+const pN7 = Vec3.zero()
+const pC8 = Vec3.zero()
+const pN9 = Vec3.zero()
+const normal = Vec3.zero()
+
+export const NucleotideRingMeshParams = {
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+    detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
+}
+export const DefaultNucleotideRingMeshProps = PD.getDefaultValues(NucleotideRingMeshParams)
+export type NucleotideRingProps = typeof DefaultNucleotideRingMeshProps
+
+const positionsRing5_6 = new Float32Array(2 * 9 * 3)
+const stripIndicesRing5_6 = new Uint32Array([0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 14, 15, 12, 13, 8, 9, 10, 11, 0, 1])
+const fanIndicesTopRing5_6 = new Uint32Array([8, 12, 14, 16, 6, 4, 2, 0, 10])
+const fanIndicesBottomRing5_6 = new Uint32Array([9, 11, 1, 3, 5, 7, 17, 15, 13])
+
+const positionsRing6 = new Float32Array(2 * 6 * 3)
+const stripIndicesRing6 = new Uint32Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1])
+const fanIndicesTopRing6 = new Uint32Array([0, 10, 8, 6, 4, 2])
+const fanIndicesBottomRing6 = new Uint32Array([1, 3, 5, 7, 9, 11])
+
+const tmpShiftV = Vec3.zero()
+function shiftPositions(out: NumberArray, dir: Vec3, ...positions: Vec3[]) {
+    for (let i = 0, il = positions.length; i < il; ++i) {
+        const v = positions[i]
+        Vec3.toArray(Vec3.add(tmpShiftV, v, dir), out, (i * 2) * 3)
+        Vec3.toArray(Vec3.sub(tmpShiftV, v, dir), out, (i * 2 + 1) * 3)
+    }
+}
+
+function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: NucleotideRingProps, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
+
+    const nucleotideElementCount = unit.nucleotideElements.length
+    if (!nucleotideElementCount) return Mesh.createEmpty(mesh)
+
+    const { sizeFactor, radialSegments, detail } = props
+
+    const vertexCount = nucleotideElementCount * (26 + radialSegments * 2)
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
+
+    const { elements, model } = unit
+    const { modifiedResidues } = model.properties
+    const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
+    const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
+    const { label_comp_id } = residues
+    const pos = unit.conformation.invariantPosition
+
+    const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+
+    const radius = 1 * sizeFactor
+    const halfThickness = 1.25 * sizeFactor
+    const cylinderProps: CylinderProps = { radiusTop: 1 * sizeFactor, radiusBottom: 1 * sizeFactor, radialSegments }
+
+    let i = 0
+    while (chainIt.hasNext) {
+        residueIt.setSegment(chainIt.move());
+
+        while (residueIt.hasNext) {
+            const { index: residueIndex } = residueIt.move();
+
+            if (isNucleic(moleculeType[residueIndex])) {
+                let compId = label_comp_id.value(residueIndex)
+                const parentId = modifiedResidues.parentId.get(compId)
+                if (parentId !== undefined) compId = parentId
+
+                let idxTrace: ElementIndex | -1 = -1, idxN1: ElementIndex | -1 = -1, idxC2: ElementIndex | -1 = -1, idxN3: ElementIndex | -1 = -1, idxC4: ElementIndex | -1 = -1, idxC5: ElementIndex | -1 = -1, idxC6: ElementIndex | -1 = -1, idxN7: ElementIndex | -1 = -1, idxC8: ElementIndex | -1 = -1, idxN9: ElementIndex | -1 = -1
+
+                builderState.currentGroup = i
+
+                if (isPurinBase(compId)) {
+                    idxTrace = traceElementIndex[residueIndex]
+                    idxN1 = atomicIndex.findAtomOnResidue(residueIndex, 'N1')
+                    idxC2 = atomicIndex.findAtomOnResidue(residueIndex, 'C2')
+                    idxN3 = atomicIndex.findAtomOnResidue(residueIndex, 'N3')
+                    idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4')
+                    idxC5 = atomicIndex.findAtomOnResidue(residueIndex, 'C5')
+                    idxC6 = atomicIndex.findAtomOnResidue(residueIndex, 'C6')
+                    idxN7 = atomicIndex.findAtomOnResidue(residueIndex, 'N7')
+                    idxC8 = atomicIndex.findAtomOnResidue(residueIndex, 'C8')
+                    idxN9 = atomicIndex.findAtomOnResidue(residueIndex, 'N9')
+
+                    if (idxN9 !== -1 && idxTrace !== -1) {
+                        pos(idxN9, pN9); pos(idxTrace, pTrace)
+                        builderState.currentGroup = i
+                        addCylinder(builderState, pN9, pTrace, 1, cylinderProps)
+                        addSphere(builderState, pN9, radius, detail)
+                    }
+
+                    if (idxN1 !== -1 && idxC2 !== -1 && idxN3 !== -1 && idxC4 !== -1 && idxC5 !== -1 && idxC6 !== -1 && idxN7 !== -1 && idxC8 !== -1 && idxN9 !== -1 ) {
+                        pos(idxN1, pN1); pos(idxC2, pC2); pos(idxN3, pN3); pos(idxC4, pC4); pos(idxC5, pC5); pos(idxC6, pC6); pos(idxN7, pN7); pos(idxC8, pC8)
+
+                        Vec3.triangleNormal(normal, pN1, pC4, pC5)
+                        Vec3.scale(normal, normal, halfThickness)
+                        shiftPositions(positionsRing5_6, normal, pN1, pC2, pN3, pC4, pC5, pC6, pN7, pC8, pN9)
+
+                        MeshBuilder.addTriangleStrip(builderState, positionsRing5_6, stripIndicesRing5_6)
+                        MeshBuilder.addTriangleFan(builderState, positionsRing5_6, fanIndicesTopRing5_6)
+                        MeshBuilder.addTriangleFan(builderState, positionsRing5_6, fanIndicesBottomRing5_6)
+                    }
+                } else if (isPyrimidineBase(compId)) {
+                    idxTrace = traceElementIndex[residueIndex]
+                    idxN1 = atomicIndex.findAtomOnResidue(residueIndex, 'N1')
+                    idxC2 = atomicIndex.findAtomOnResidue(residueIndex, 'C2')
+                    idxN3 = atomicIndex.findAtomOnResidue(residueIndex, 'N3')
+                    idxC4 = atomicIndex.findAtomOnResidue(residueIndex, 'C4')
+                    idxC5 = atomicIndex.findAtomOnResidue(residueIndex, 'C5')
+                    idxC6 = atomicIndex.findAtomOnResidue(residueIndex, 'C6')
+
+                    if (idxN1 !== -1 && idxTrace !== -1) {
+                        pos(idxN1, pN1); pos(idxTrace, pTrace)
+                        builderState.currentGroup = i
+                        addCylinder(builderState, pN1, pTrace, 1, cylinderProps)
+                        addSphere(builderState, pN1, radius, detail)
+                    }
+
+                    if (idxN1 !== -1 && idxC2 !== -1 && idxN3 !== -1 && idxC4 !== -1 && idxC5 !== -1 && idxC6 !== -1) {
+                        pos(idxC2, pC2); pos(idxN3, pN3); pos(idxC4, pC4); pos(idxC5, pC5); pos(idxC6, pC6);
+
+                        Vec3.triangleNormal(normal, pN1, pC4, pC5)
+                        Vec3.scale(normal, normal, halfThickness)
+                        shiftPositions(positionsRing6, normal, pN1, pC2, pN3, pC4, pC5, pC6)
+
+                        MeshBuilder.addTriangleStrip(builderState, positionsRing6, stripIndicesRing6)
+                        MeshBuilder.addTriangleFan(builderState, positionsRing6, fanIndicesTopRing6)
+                        MeshBuilder.addTriangleFan(builderState, positionsRing6, fanIndicesBottomRing6)
+                    }
+                }
+
+                ++i
+            }
+        }
+    }
+
+    return MeshBuilder.getMesh(builderState)
+}
+
+export const NucleotideRingParams = {
+    ...UnitsMeshParams,
+    ...NucleotideRingMeshParams
+}
+export type NucleotideRingParams = typeof NucleotideRingParams
+
+export function NucleotideRingVisual(): UnitsVisual<NucleotideRingParams> {
+    return UnitsMeshVisual<NucleotideRingParams>({
+        defaultProps: PD.getDefaultValues(NucleotideRingParams),
+        createGeometry: createNucleotideRingMesh,
+        createLocationIterator: NucleotideLocationIterator.fromGroup,
+        getLoci: getNucleotideElementLoci,
+        eachLocation: eachNucleotideElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NucleotideRingParams>, currentProps: PD.Values<NucleotideRingParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
+        }
+    })
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
index 60e8ba403f1fa0433a258b7f53724a1221ce09e6..8584245222c4ebd57889f01f13c254ab3790bf40 100644
--- a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
@@ -8,7 +8,7 @@ import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
 import { PolymerBackboneIterator } from './util/polymer';
-import { getElementLoci, markElement, StructureElementIterator } from './util/element';
+import { getElementLoci, eachElement, StructureElementIterator } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
 import { OrderedSet } from 'mol-data/int';
@@ -74,7 +74,7 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> {
         // TODO create a specialized location iterator
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
-        mark: markElement,
+        eachLocation: eachElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerBackboneParams>, currentProps: PD.Values<PolymerBackboneParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
diff --git a/src/mol-repr/structure/visual/polymer-direction-wedge.ts b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
index d94e90efbc3714750b118292adc7c27b7cc5921a..ab3e037548ced5b766f2c8b630fd14b14697310d 100644
--- a/src/mol-repr/structure/visual/polymer-direction-wedge.ts
+++ b/src/mol-repr/structure/visual/polymer-direction-wedge.ts
@@ -6,7 +6,7 @@
 
 import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
-import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, eachPolymerElement } from './util/polymer';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
@@ -98,7 +98,7 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> {
         createGeometry: createPolymerDirectionWedgeMesh,
         createLocationIterator: PolymerLocationIterator.fromGroup,
         getLoci: getPolymerElementLoci,
-        mark: markPolymerElement,
+        eachLocation: eachPolymerElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerDirectionParams>, currentProps: PD.Values<PolymerDirectionParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor
diff --git a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
index d2ebb496edcf15f62469ec34e66c00696b02f8b2..dc80fba747b4d4de619b47257899b0efc3ec3a14 100644
--- a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
+++ b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts
@@ -7,7 +7,7 @@
 import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { PolymerGapIterator, PolymerGapLocationIterator, markPolymerGapElement, getPolymerGapElementLoci } from './util/polymer';
+import { PolymerGapIterator, PolymerGapLocationIterator, eachPolymerGapElement, getPolymerGapElementLoci } from './util/polymer';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -91,7 +91,7 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> {
         createGeometry: createPolymerGapCylinderMesh,
         createLocationIterator: PolymerGapLocationIterator.fromGroup,
         getLoci: getPolymerGapElementLoci,
-        mark: markPolymerGapElement,
+        eachLocation: eachPolymerGapElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerGapParams>, currentProps: PD.Values<PolymerGapParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
index deb5042742b2b45d282c1fedbbca8d1285f26f95..dfbd3b6a363862434493f0977d83a5a362ab0b09 100644
--- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts
+++ b/src/mol-repr/structure/visual/polymer-trace-mesh.ts
@@ -7,7 +7,7 @@
 import { Unit, Structure } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { VisualUpdateState } from '../../util';
-import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, eachPolymerElement, interpolateSizes } from './util/polymer';
 import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -41,7 +41,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
 
     const isCoarse = Unit.isCoarse(unit)
     const state = createCurveSegmentState(linearSegments)
-    const { curvePoints, normalVectors, binormalVectors } = state
+    const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state
 
     let i = 0
     const polymerTraceIt = PolymerTraceIterator(unit)
@@ -57,24 +57,39 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
 
         interpolateCurveSegment(state, v, tension, shift)
 
-        let width = theme.size.size(v.center) * sizeFactor
-        if (isCoarse) width *= aspectRatio / 2
+        let w0 = theme.size.size(v.centerPrev) * sizeFactor
+        let w1 = theme.size.size(v.center) * sizeFactor
+        let w2 = theme.size.size(v.centerNext) * sizeFactor
+        if (isCoarse) {
+            w0 *= aspectRatio / 2
+            w1 *= aspectRatio / 2
+            w2 *= aspectRatio / 2
+        }
 
         if (isSheet) {
-            const height = width * aspectRatio
-            const arrowHeight = v.secStrucLast ? height * arrowFactor : 0
-            addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, v.secStrucFirst, v.secStrucLast)
+            const h1 = w1 * aspectRatio
+            const arrowHeight = v.secStrucLast ? h1 * arrowFactor : 0
+            addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, w1, h1, arrowHeight, v.secStrucFirst, v.secStrucLast)
         } else {
-            let height: number
-            if (isHelix) {
-                height = width * aspectRatio
-            } else if (isNucleicType) {
-                height = width * aspectRatio;
-                [width, height] = [height, width]
+            let h0: number, h1: number, h2: number
+            if (isHelix && !v.isCoarseBackbone) {
+                h0 = w0 * aspectRatio
+                h1 = w1 * aspectRatio
+                h2 = w2 * aspectRatio
+            } else if (isNucleicType && !v.isCoarseBackbone) {
+                h0 = w0 * aspectRatio;
+                [w0, h0] = [h0, w0]
+                h1 = w1 * aspectRatio;
+                [w1, h1] = [h1, w1]
+                h2 = w2 * aspectRatio;
+                [w2, h2] = [h2, w2]
             } else {
-                height = width
+                h0 = w0
+                h1 = w1
+                h2 = w2
             }
-            addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, v.secStrucFirst, v.secStrucLast)
+            interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift)
+            addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.secStrucFirst || v.coarseBackboneFirst, v.secStrucLast || v.coarseBackboneLast)
         }
 
         ++i
@@ -95,7 +110,7 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> {
         createGeometry: createPolymerTraceMesh,
         createLocationIterator: PolymerLocationIterator.fromGroup,
         getLoci: getPolymerElementLoci,
-        mark: markPolymerElement,
+        eachLocation: eachPolymerElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTraceParams>, currentProps: PD.Values<PolymerTraceParams>) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
diff --git a/src/mol-repr/structure/visual/polymer-tube-mesh.ts b/src/mol-repr/structure/visual/polymer-tube-mesh.ts
new file mode 100644
index 0000000000000000000000000000000000000000..94accbff631fddd68ac6de7a8b8640d53f64a7d2
--- /dev/null
+++ b/src/mol-repr/structure/visual/polymer-tube-mesh.ts
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Structure } from 'mol-model/structure';
+import { UnitsVisual } from '../representation';
+import { VisualUpdateState } from '../../util';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, eachPolymerElement, interpolateSizes } from './util/polymer';
+import { isNucleic } from 'mol-model/structure/model/types';
+import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
+import { addTube } from 'mol-geo/geometry/mesh/builder/tube';
+import { VisualContext } from 'mol-repr/visual';
+import { Theme } from 'mol-theme/theme';
+
+export const PolymerTubeMeshParams = {
+    sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
+}
+export const DefaultPolymerTubeMeshProps = PD.getDefaultValues(PolymerTubeMeshParams)
+export type PolymerTubeMeshProps = typeof DefaultPolymerTubeMeshProps
+
+// TODO handle polymer ends properly
+
+function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PolymerTubeMeshProps, mesh?: Mesh) {
+    const polymerElementCount = unit.polymerElements.length
+
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+    const { sizeFactor, linearSegments, radialSegments } = props
+
+    const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh)
+
+    const state = createCurveSegmentState(linearSegments)
+    const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state
+
+    let i = 0
+    const polymerTraceIt = PolymerTraceIterator(unit)
+    while (polymerTraceIt.hasNext) {
+        const v = polymerTraceIt.move()
+        builderState.currentGroup = i
+
+        const isNucleicType = isNucleic(v.moleculeType)
+        const tension = isNucleicType ? 0.5 : 0.9
+        const shift = isNucleicType ? 0.3 : 0.5
+
+        interpolateCurveSegment(state, v, tension, shift)
+
+        let s0 = theme.size.size(v.centerPrev) * sizeFactor
+        let s1 = theme.size.size(v.center) * sizeFactor
+        let s2 = theme.size.size(v.centerNext) * sizeFactor
+
+        interpolateSizes(state, s0, s1, s2, s0, s1, s2, shift)
+        addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, 1, v.first, v.last)
+
+        ++i
+    }
+
+    return MeshBuilder.getMesh(builderState)
+}
+
+export const PolymerTubeParams = {
+    ...UnitsMeshParams,
+    ...PolymerTubeMeshParams
+}
+export type PolymerTubeParams = typeof PolymerTubeParams
+
+export function PolymerTubeVisual(): UnitsVisual<PolymerTubeParams> {
+    return UnitsMeshVisual<PolymerTubeParams>({
+        defaultProps: PD.getDefaultValues(PolymerTubeParams),
+        createGeometry: createPolymerTubeMesh,
+        createLocationIterator: PolymerLocationIterator.fromGroup,
+        getLoci: getPolymerElementLoci,
+        eachLocation: eachPolymerElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTubeParams>, currentProps: PD.Values<PolymerTubeParams>) => {
+            state.createGeometry = (
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.linearSegments !== currentProps.linearSegments ||
+                newProps.radialSegments !== currentProps.radialSegments
+            )
+        }
+    })
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts
index 1e2f479d22e546a668aab603854caf7841db563f..cd0e2271a17f32e5821ebca489f0f935eb15c4a8 100644
--- a/src/mol-repr/structure/visual/util/common.ts
+++ b/src/mol-repr/structure/visual/util/common.ts
@@ -11,7 +11,7 @@ import { OrderedSet, SortedArray } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 
 /** Return a Loci for the elements of a whole residue the elementIndex belongs to. */
-export function getResidueLoci(structure: Structure, unit: Unit, elementIndex: ElementIndex): Loci {
+export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
     const { elements, model } = unit
     if (OrderedSet.indexOf(elements, elementIndex) !== -1) {
         const { index, offsets } = model.atomicHierarchy.residueAtomSegments
diff --git a/src/mol-repr/structure/visual/util/element.ts b/src/mol-repr/structure/visual/util/element.ts
index ae67e9c7aafdcbd6070d58120e62973c94ef4d6f..1ce958ec96e37b0cf6612f086c7ec72132c50dda 100644
--- a/src/mol-repr/structure/visual/util/element.ts
+++ b/src/mol-repr/structure/visual/util/element.ts
@@ -55,15 +55,12 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
 
     const { elements } = unit;
     const elementCount = elements.length;
-    const builder = SpheresBuilder.create(elementCount, elementCount / 2)
+    const builder = SpheresBuilder.create(elementCount, elementCount / 2, spheres)
 
     const v = Vec3.zero()
     const pos = unit.conformation.invariantPosition
-    const l = StructureElement.create()
-    l.unit = unit
 
     for (let i = 0; i < elementCount; i++) {
-        l.element = elements[i]
         pos(elements[i], v)
         builder.add(v[0], v[1], v[2], i)
     }
@@ -71,11 +68,11 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
     return builder.getSpheres()
 }
 
-export function markElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+export function eachElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
     const { structure, group } = structureGroup
-    if (loci.structure !== structure) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
     const elementCount = group.elements.length
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
diff --git a/src/mol-repr/structure/visual/util/gaussian.ts b/src/mol-repr/structure/visual/util/gaussian.ts
index 54cd72a4b8ba4dcf581d87842432de530588078b..9cee5dcdddacc77754afb886ad4829538ea46779 100644
--- a/src/mol-repr/structure/visual/util/gaussian.ts
+++ b/src/mol-repr/structure/visual/util/gaussian.ts
@@ -1,5 +1,126 @@
 /**
- * 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 { Unit, StructureElement, ElementIndex, Structure } from 'mol-model/structure';
+import { GaussianDensity } from 'mol-math/geometry/gaussian-density';
+import { Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { GaussianDensityTexture } from 'mol-math/geometry/gaussian-density/gpu';
+import { Texture } from 'mol-gl/webgl/texture';
+import { WebGLContext } from 'mol-gl/webgl/context';
+import { PhysicalSizeTheme } from 'mol-theme/size/physical';
+import { OrderedSet } from 'mol-data/int';
+
+export const GaussianDensityParams = {
+    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
+    smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
+    useGpu: PD.Boolean(false),
+}
+export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams)
+export type GaussianDensityProps = typeof DefaultGaussianDensityProps
+
+export const GaussianDensityTextureParams = {
+    resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
+    smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
+}
+export const DefaultGaussianDensityTextureProps = PD.getDefaultValues(GaussianDensityTextureParams)
+export type GaussianDensityTextureProps = typeof DefaultGaussianDensityTextureProps
+
+//
+
+function getConformation(unit: Unit) {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return unit.model.atomicConformation
+        case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres
+        case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians
+    }
+}
+
+function getUnitConformationAndRadius(unit: Unit) {
+    const conformation = getConformation(unit)
+    const { elements } = unit
+    const position = {
+        indices: elements,
+        x: conformation.x,
+        y: conformation.y,
+        z: conformation.z
+    }
+
+    const l = StructureElement.create(unit)
+    const sizeTheme = PhysicalSizeTheme({}, {})
+    const radius = (index: number) => {
+        l.element = index as ElementIndex
+        return sizeTheme.size(l)
+    }
+
+    return { position, radius }
+}
+
+export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) {
+    const { position, radius } = getUnitConformationAndRadius(unit)
+    return Task.create('Gaussian Density', async ctx => {
+        return await GaussianDensity(ctx, position, unit.lookup3d.boundary.box, radius, props, webgl);
+    });
+}
+
+export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
+    const { position, radius } = getUnitConformationAndRadius(unit)
+    return Task.create('Gaussian Density', async ctx => {
+        return await GaussianDensityTexture(ctx, webgl, position, unit.lookup3d.boundary.box, radius, props, texture);
+    });
+}
+
+//
+
+function getStructureConformationAndRadius(structure: Structure) {
+    const n = structure.elementCount
+
+    const xs = new Float32Array(n)
+    const ys = new Float32Array(n)
+    const zs = new Float32Array(n)
+    const rs = new Float32Array(n)
+
+    const l = StructureElement.create()
+    const sizeTheme = PhysicalSizeTheme({}, {})
+
+    let m = 0
+    for (let i = 0, il = structure.units.length; i < il; ++i) {
+        const unit = structure.units[i]
+        const { elements } = unit
+        const { x, y, z } = unit.conformation
+        l.unit = unit
+        for (let j = 0, jl = elements.length; j < jl; ++j) {
+            const eI = elements[j]
+            xs[m + j] = x(eI)
+            ys[m + j] = y(eI)
+            zs[m + j] = z(eI)
+            l.element = eI
+            rs[m + j] = sizeTheme.size(l)
+        }
+        m += elements.length
+    }
+
+    const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs }
+    const radius = (index: number) => rs[index]
+
+    return { position, radius }
+}
+
+export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps, webgl?: WebGLContext) {
+    const { position, radius } = getStructureConformationAndRadius(structure)
+    return Task.create('Gaussian Density', async ctx => {
+        return await GaussianDensity(ctx, position, structure.lookup3d.boundary.box, radius, props, webgl);
+    });
+}
+
+export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+    const { position, radius } = getStructureConformationAndRadius(structure)
+    return Task.create('Gaussian Density', async ctx => {
+        return await GaussianDensityTexture(ctx, webgl, position, structure.lookup3d.boundary.box, radius, props, texture);
+    });
+}
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/link.ts b/src/mol-repr/structure/visual/util/link.ts
index 956f617281fabe76a138e3f77acd0a5aa18689fa..df61f267d2e2a0da909d8409aab8b349de8ecd36 100644
--- a/src/mol-repr/structure/visual/util/link.ts
+++ b/src/mol-repr/structure/visual/util/link.ts
@@ -29,7 +29,6 @@ const tmpShiftV13 = Vec3.zero()
 /** Calculate 'shift' direction that is perpendiculat to v1 - v2 and goes through v3 */
 export function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | null) {
     Vec3.normalize(tmpShiftV12, Vec3.sub(tmpShiftV12, v1, v2))
-
     if (v3 !== null) {
         Vec3.sub(tmpShiftV13, v1, v3)
     } else {
@@ -79,7 +78,13 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
     const va = Vec3.zero()
     const vb = Vec3.zero()
     const vShift = Vec3.zero()
-    const cylinderProps: CylinderProps = { radiusTop: 1, radiusBottom: 1, radialSegments }
+    const cylinderProps: CylinderProps = {
+        radiusTop: 1,
+        radiusBottom: 1,
+        radialSegments,
+        topCap: false,
+        bottomCap: false
+    }
 
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         position(va, vb, edgeIndex)
@@ -92,6 +97,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
         if (LinkType.is(f, LinkType.Flag.MetallicCoordination) || LinkType.is(f, LinkType.Flag.Hydrogen)) {
             // show metall coordinations and hydrogen bonds with dashed cylinders
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3
+            cylinderProps.topCap = cylinderProps.bottomCap = true
             addFixedCountDashedCylinder(builderState, va, vb, 0.5, 7, cylinderProps)
         } else if (o === 2 || o === 3) {
             // show bonds with order 2 or 3 using 2 or 3 parallel cylinders
@@ -102,11 +108,13 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
             Vec3.setMagnitude(vShift, vShift, absOffset)
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius
+            cylinderProps.topCap = cylinderProps.bottomCap = false
 
             if (o === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps)
             addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps)
         } else {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius
+            cylinderProps.topCap = cylinderProps.bottomCap = false
             addCylinder(builderState, va, vb, 0.5, cylinderProps)
         }
     }
diff --git a/src/mol-repr/structure/visual/util/nucleotide.ts b/src/mol-repr/structure/visual/util/nucleotide.ts
index 44ce2c3e8638cb616ec6aed922d9565e1aed2617..22fe970f1692264dadea9354d0b454aa44fea7bc 100644
--- a/src/mol-repr/structure/visual/util/nucleotide.ts
+++ b/src/mol-repr/structure/visual/util/nucleotide.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, StructureElement } from 'mol-model/structure';
+import { Unit, StructureElement, Structure } from 'mol-model/structure';
 import { getNucleotideElements } from 'mol-model/structure/structure/util/nucleotide';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { OrderedSet, Interval } from 'mol-data/int';
@@ -40,11 +40,11 @@ export function getNucleotideElementLoci(pickingId: PickingId, structureGroup: S
     return EmptyLoci
 }
 
-export function markNucleotideElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+export function eachNucleotideElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
     const { structure, group } = structureGroup
-    if (loci.structure !== structure) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
     const unit = group.units[0]
     if (!Unit.isAtomic(unit)) return false
     const { nucleotideElements, model, elements } = unit
diff --git a/src/mol-repr/structure/visual/util/polymer.ts b/src/mol-repr/structure/visual/util/polymer.ts
index 288bf8527c97cf1344c9de45a6eccfacccf7d473..bf0f536b8c15cbbd1f4f98788507fd9fa6807e4e 100644
--- a/src/mol-repr/structure/visual/util/polymer.ts
+++ b/src/mol-repr/structure/visual/util/polymer.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, ElementIndex, StructureElement, Link } from 'mol-model/structure';
+import { Unit, ElementIndex, StructureElement, Link, Structure } from 'mol-model/structure';
 import SortedRanges from 'mol-data/int/sorted-ranges';
 import { OrderedSet, Interval } from 'mol-data/int';
 import { EmptyLoci, Loci } from 'mol-model/loci';
@@ -72,17 +72,30 @@ export function getPolymerElementLoci(pickingId: PickingId, structureGroup: Stru
     if (id === objectId) {
         const { structure, group } = structureGroup
         const unit = group.units[instanceId]
-        return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        if (Unit.isAtomic(unit)) {
+            return getResidueLoci(structure, unit, unit.polymerElements[groupId])
+        } else {
+            const { elements } = unit
+            const elementIndex = unit.polymerElements[groupId]
+            const unitIndex = OrderedSet.indexOf(elements, elementIndex) as StructureElement.UnitIndex | -1
+            if (unitIndex !== -1) {
+                const indices = OrderedSet.ofSingleton(unitIndex)
+                return StructureElement.Loci(structure, [{ unit, indices }])
+            }
+        }
     }
     return EmptyLoci
 }
 
-/** Mark a polymer element (e.g. part of a cartoon trace) when all its residue's elements are in a loci. */
-export function markPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+/**
+ * Mark a polymer element (e.g. part of a cartoon trace)
+ * - for atomic units mark only when all its residue's elements are in a loci
+ */
+export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!StructureElement.isLoci(loci)) return false
     const { structure, group } = structureGroup
-    if (loci.structure !== structure) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
     const { polymerElements, model, elements } = group.units[0]
     const { index, offsets } = model.atomicHierarchy.residueAtomSegments
     const { traceElementIndex } = model.atomicHierarchy.derived.residue
@@ -90,19 +103,32 @@ export function markPolymerElement(loci: Loci, structureGroup: StructureGroup, a
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
         if (unitIdx !== undefined) {
-            // TODO optimized implementation for intervals
-            OrderedSet.forEach(e.indices, v => {
-                const rI = index[elements[v]]
-                const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
-                const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
-                const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-                const eI = traceElementIndex[rI]
-                const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
-                if (idx !== -1) {
-                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+            if (Unit.isAtomic(e.unit)) {
+                // TODO optimized implementation for intervals
+                OrderedSet.forEach(e.indices, v => {
+                    const rI = index[elements[v]]
+                    const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
+                    const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
+                    const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
+                    if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                    const eI = traceElementIndex[rI]
+                    const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
+                    if (idx !== -1) {
+                        if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                    }
+                })
+            } else {
+                if (Interval.is(e.indices)) {
+                    const start = unitIdx * groupCount + Interval.start(e.indices);
+                    const end = unitIdx * groupCount + Interval.end(e.indices);
+                    if (apply(Interval.ofBounds(start, end))) changed = true
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = unitIdx * groupCount + e.indices[i];
+                        if (apply(Interval.ofSingleton(idx))) changed = true
+                    }
                 }
-            })
+            }
         }
     }
     return changed
@@ -126,11 +152,11 @@ export function getPolymerGapElementLoci(pickingId: PickingId, structureGroup: S
     return EmptyLoci
 }
 
-export function markPolymerGapElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
+export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
     let changed = false
     if (!Link.isLoci(loci)) return false
     const { structure, group } = structureGroup
-    if (loci.structure !== structure) return false
+    if (!Structure.areEquivalent(loci.structure, structure)) return false
     const groupCount = group.units[0].gapElements.length
     for (const b of loci.links) {
         const unitIdx = group.unitIndexMap.get(b.aUnit.id)
diff --git a/src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts b/src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts
index f80b306af030cd83547bfa2cf82fb88f470ca0d1..103246a32fcc74c9c85abd2a43bb4e9bbf4a7c6e 100644
--- a/src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts
+++ b/src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts
@@ -81,7 +81,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
     }
 
     constructor(private unit: Unit.Atomic) {
-        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex
+        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex> // can assume it won't be -1 for polymer residues
         this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements)
         this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements)
         this.value = createPolymerBackbonePair(unit)
diff --git a/src/mol-repr/structure/visual/util/polymer/curve-segment.ts b/src/mol-repr/structure/visual/util/polymer/curve-segment.ts
index 1231c6bb0f9e160f9ed62b36cae038b867f9c2f8..07ebb90f650415900756038b8a3d779e3df85a9a 100644
--- a/src/mol-repr/structure/visual/util/polymer/curve-segment.ts
+++ b/src/mol-repr/structure/visual/util/polymer/curve-segment.ts
@@ -6,12 +6,15 @@
 
 import { Vec3 } from 'mol-math/linear-algebra';
 import { NumberArray } from 'mol-util/type-helpers';
+import { lerp } from 'mol-math/interpolate';
 
 export interface CurveSegmentState {
     curvePoints: NumberArray,
     tangentVectors: NumberArray,
     normalVectors: NumberArray,
     binormalVectors: NumberArray,
+    widthValues: NumberArray,
+    heightValues: NumberArray,
     linearSegments: number
 }
 
@@ -21,12 +24,15 @@ export interface CurveSegmentControls {
 }
 
 export function createCurveSegmentState(linearSegments: number): CurveSegmentState {
-    const pn = (linearSegments + 1) * 3
+    const n = linearSegments + 1
+    const pn = n * 3
     return {
         curvePoints: new Float32Array(pn),
         tangentVectors: new Float32Array(pn),
         normalVectors: new Float32Array(pn),
         binormalVectors: new Float32Array(pn),
+        widthValues: new Float32Array(n),
+        heightValues: new Float32Array(n),
         linearSegments
     }
 }
@@ -112,4 +118,21 @@ export function interpolateNormals(state: CurveSegmentState, controls: CurveSegm
         Vec3.normalize(binormalVec, Vec3.cross(binormalVec, tangentVec, normalVec))
         Vec3.toArray(binormalVec, binormalVectors, i * 3)
     }
+}
+
+export function interpolateSizes(state: CurveSegmentState, w0: number, w1: number, w2: number, h0: number, h1: number, h2: number, shift: number) {
+    const { widthValues, heightValues, linearSegments } = state
+
+    const shift1 = 1 - shift
+
+    for (let i = 0; i <= linearSegments; ++i) {
+        const t = i * 1.0 / linearSegments;
+        if (t < shift1) {
+            widthValues[i] = lerp(w0, w1, t + shift)
+            heightValues[i] = lerp(h0, h1, t + shift)
+        } else {
+            widthValues[i] = lerp(w1, w2, t - shift1)
+            heightValues[i] = lerp(h1, h2, t - shift1)
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-repr/structure/visual/util/polymer/gap-iterator.ts b/src/mol-repr/structure/visual/util/polymer/gap-iterator.ts
index bc15bb9f5e71df6d78ece3a9d42541fabcb45f90..6a612ab598af2b92c1213a59d37a6eb6ce7de3f3 100644
--- a/src/mol-repr/structure/visual/util/polymer/gap-iterator.ts
+++ b/src/mol-repr/structure/visual/util/polymer/gap-iterator.ts
@@ -47,7 +47,7 @@ export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> {
     }
 
     constructor(private unit: Unit.Atomic) {
-        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex
+        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex> // can assume it won't be -1 for polymer residues
         this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements);
         this.value = createPolymerGapPair(unit)
         this.hasNext = this.gapIt.hasNext
diff --git a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
index e2c95b035dc03635e43d342cd1fb4ef62e3ae118..6216c5307bf739bda64629d24387b3754b68e94a 100644
--- a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
+++ b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
@@ -29,10 +29,14 @@ export function PolymerTraceIterator(unit: Unit): Iterator<PolymerTraceElement>
 
 interface PolymerTraceElement {
     center: StructureElement
+    centerPrev: StructureElement
+    centerNext: StructureElement
     first: boolean, last: boolean
     secStrucFirst: boolean, secStrucLast: boolean
     secStrucType: SecondaryStructureType
     moleculeType: MoleculeType
+    isCoarseBackbone: boolean
+    coarseBackboneFirst: boolean, coarseBackboneLast: boolean
 
     p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3
     d12: Vec3, d23: Vec3
@@ -43,10 +47,14 @@ const SecStrucTypeNA = SecondaryStructureType.create(SecondaryStructureType.Flag
 function createPolymerTraceElement (unit: Unit): PolymerTraceElement {
     return {
         center: StructureElement.create(unit),
+        centerPrev: StructureElement.create(unit),
+        centerNext: StructureElement.create(unit),
         first: false, last: false,
         secStrucFirst: false, secStrucLast: false,
         secStrucType: SecStrucTypeNA,
         moleculeType: MoleculeType.unknown,
+        coarseBackboneFirst: false, coarseBackboneLast: false,
+        isCoarseBackbone: false,
         p0: Vec3.zero(), p1: Vec3.zero(), p2: Vec3.zero(), p3: Vec3.zero(), p4: Vec3.zero(),
         d12: Vec3.create(1, 0, 0), d23: Vec3.create(1, 0, 0),
     }
@@ -66,10 +74,13 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     private prevSecStrucType: SecondaryStructureType
     private currSecStrucType: SecondaryStructureType
     private nextSecStrucType: SecondaryStructureType
+    private prevCoarseBackbone: boolean
+    private currCoarseBackbone: boolean
+    private nextCoarseBackbone: boolean
     private state: AtomicPolymerTraceIteratorState = AtomicPolymerTraceIteratorState.nextPolymer
     private residueAtomSegments: Segmentation<ElementIndex, ResidueIndex>
     private traceElementIndex: ArrayLike<ElementIndex>
-    private directionElementIndex: ArrayLike<ElementIndex>
+    private directionElementIndex: ArrayLike<ElementIndex | -1>
     private moleculeType: ArrayLike<MoleculeType>
     private atomicConformation: AtomicConformation
 
@@ -82,16 +93,18 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     private p6 = Vec3.zero();
 
     // private v01 = Vec3.zero();
-    private v12 = Vec3.zero();
-    private v23 = Vec3.zero();
+    private v12 = Vec3.create(1, 0, 0);
+    private v23 = Vec3.create(1, 0, 0);
     // private v34 = Vec3.zero();
 
     hasNext: boolean = false;
 
     private pos(target: Vec3, index: number) {
-        target[0] = this.atomicConformation.x[index]
-        target[1] = this.atomicConformation.y[index]
-        target[2] = this.atomicConformation.z[index]
+        if (index !== -1) {
+            target[0] = this.atomicConformation.x[index]
+            target[1] = this.atomicConformation.y[index]
+            target[2] = this.atomicConformation.z[index]
+        }
     }
 
     private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<ResidueIndex>) {
@@ -140,6 +153,8 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
                     this.state = AtomicPolymerTraceIteratorState.nextResidue
                     this.currSecStrucType = SecStrucTypeNA
                     this.nextSecStrucType = this.secondaryStructureType[this.residueSegmentMin]
+                    this.currCoarseBackbone = false
+                    this.nextCoarseBackbone = this.directionElementIndex[this.residueSegmentMin] === -1
                     break
                 }
             }
@@ -151,24 +166,43 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
             this.currSecStrucType = this.nextSecStrucType
             this.nextSecStrucType = residueIt.hasNext ? this.secondaryStructureType[residueIndex + 1] : SecStrucTypeNA
 
+            this.prevCoarseBackbone = this.currCoarseBackbone
+            this.currCoarseBackbone = this.nextCoarseBackbone
+            this.nextCoarseBackbone = residueIt.hasNext ? this.directionElementIndex[residueIndex + 1] === -1 : false
+
             value.secStrucType = this.currSecStrucType
-            value.center.element = this.traceElementIndex[residueIndex]
-            value.first = residueIndex === this.residueSegmentMin
-            value.last = residueIndex === this.residueSegmentMax
             value.secStrucFirst = this.prevSecStrucType !== this.currSecStrucType
             value.secStrucLast = this.currSecStrucType !== this.nextSecStrucType
+            value.isCoarseBackbone = this.currCoarseBackbone
+            value.coarseBackboneFirst = this.prevCoarseBackbone !== this.currCoarseBackbone
+            value.coarseBackboneLast = this.currCoarseBackbone !== this.nextCoarseBackbone
+            value.first = residueIndex === this.residueSegmentMin
+            value.last = residueIndex === this.residueSegmentMax
             value.moleculeType = this.moleculeType[residueIndex]
 
+            const residueIndexPrev3 = this.getResidueIndex(residueIndex - 3)
+            const residueIndexPrev2 = this.getResidueIndex(residueIndex - 2)
+            const residueIndexPrev1 = this.getResidueIndex(residueIndex - 1)
+            const residueIndexNext1 = this.getResidueIndex(residueIndex + 1)
+            const residueIndexNext2 = this.getResidueIndex(residueIndex + 2)
+            const residueIndexNext3 = this.getResidueIndex(residueIndex + 3)
+
             if (value.first) {
-                this.pos(this.p0, this.traceElementIndex[this.getResidueIndex(residueIndex - 3)])
-                this.pos(this.p1, this.traceElementIndex[this.getResidueIndex(residueIndex - 2)])
-                this.pos(this.p2, this.traceElementIndex[this.getResidueIndex(residueIndex - 1)])
-                this.pos(this.p3, value.center.element)
-                this.pos(this.p4, this.traceElementIndex[this.getResidueIndex(residueIndex + 1)])
-                this.pos(this.p5, this.traceElementIndex[this.getResidueIndex(residueIndex + 2)])
-
-                this.pos(this.v12, this.directionElementIndex[this.getResidueIndex(residueIndex - 1)])
+                value.centerPrev.element = this.traceElementIndex[residueIndexPrev1]
+                value.center.element = this.traceElementIndex[residueIndex]
+
+                this.pos(this.p0, this.traceElementIndex[residueIndexPrev3])
+                this.pos(this.p1, this.traceElementIndex[residueIndexPrev2])
+                this.pos(this.p2, this.traceElementIndex[residueIndexPrev1])
+                this.pos(this.p3, this.traceElementIndex[residueIndex])
+                this.pos(this.p4, this.traceElementIndex[residueIndexNext1])
+                this.pos(this.p5, this.traceElementIndex[residueIndexNext2])
+
+                this.pos(this.v12, this.directionElementIndex[residueIndexPrev1])
             } else {
+                value.centerPrev.element = value.center.element
+                value.center.element = value.centerNext.element
+
                 Vec3.copy(this.p0, this.p1)
                 Vec3.copy(this.p1, this.p2)
                 Vec3.copy(this.p2, this.p3)
@@ -178,14 +212,16 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
                 Vec3.copy(this.v12, this.v23)
             }
-            this.pos(this.p6,  this.traceElementIndex[this.getResidueIndex(residueIndex + 3 as ResidueIndex)])
+            value.centerNext.element = this.traceElementIndex[residueIndexNext1]
+            this.pos(this.p6,  this.traceElementIndex[residueIndexNext3])
             this.pos(this.v23, this.directionElementIndex[residueIndex])
+            value.isCoarseBackbone = this.directionElementIndex[residueIndex] === -1
 
-            this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndex - 2 as ResidueIndex)
-            this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndex - 1 as ResidueIndex)
+            this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndexPrev2)
+            this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndexPrev1)
             this.setControlPoint(value.p2, this.p2, this.p3, this.p4, residueIndex)
-            this.setControlPoint(value.p3, this.p3, this.p4, this.p5, residueIndex + 1 as ResidueIndex)
-            this.setControlPoint(value.p4, this.p4, this.p5, this.p6, residueIndex + 2 as ResidueIndex)
+            this.setControlPoint(value.p3, this.p3, this.p4, this.p5, residueIndexNext1)
+            this.setControlPoint(value.p4, this.p4, this.p5, this.p6, residueIndexNext2)
 
             Vec3.copy(value.d12, this.v12)
             Vec3.copy(value.d23, this.v23)
@@ -203,7 +239,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     constructor(private unit: Unit.Atomic) {
         this.atomicConformation = unit.model.atomicConformation
         this.residueAtomSegments = unit.model.atomicHierarchy.residueAtomSegments
-        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex
+        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex as ArrayLike<ElementIndex> // can assume it won't be -1 for polymer residues
         this.directionElementIndex = unit.model.atomicHierarchy.derived.residue.directionElementIndex
         this.moleculeType = unit.model.atomicHierarchy.derived.residue.moleculeType
         this.cyclicPolymerMap = unit.model.atomicHierarchy.cyclicPolymerMap
@@ -226,8 +262,11 @@ export class CoarsePolymerTraceIterator implements Iterator<PolymerTraceElement>
     private elementIndex: number
     hasNext: boolean = false;
 
+    private getElementIndex(elementIndex: number) {
+        return Math.min(Math.max(this.polymerSegment.start, elementIndex), this.polymerSegment.end - 1) as ElementIndex
+    }
+
     private pos(target: Vec3, elementIndex: number) {
-        elementIndex = Math.min(Math.max(this.polymerSegment.start, elementIndex), this.polymerSegment.end - 1)
         const index = this.unit.elements[elementIndex]
         target[0] = this.conformation.x[index]
         target[1] = this.conformation.y[index]
@@ -249,13 +288,21 @@ export class CoarsePolymerTraceIterator implements Iterator<PolymerTraceElement>
 
         if (this.state === CoarsePolymerTraceIteratorState.nextElement) {
             this.elementIndex += 1
+
+            const elementIndexPrev2 = this.getElementIndex(this.elementIndex - 2)
+            const elementIndexPrev1 = this.getElementIndex(this.elementIndex - 1)
+            const elementIndexNext1 = this.getElementIndex(this.elementIndex + 1)
+            const elementIndexNext2 = this.getElementIndex(this.elementIndex + 2)
+
+            this.value.centerPrev.element = this.value.center.unit.elements[elementIndexPrev1]
             this.value.center.element = this.value.center.unit.elements[this.elementIndex]
+            this.value.centerNext.element = this.value.center.unit.elements[elementIndexNext1]
 
-            this.pos(this.value.p0, this.elementIndex - 2)
-            this.pos(this.value.p1, this.elementIndex - 1)
+            this.pos(this.value.p0, elementIndexPrev2)
+            this.pos(this.value.p1, elementIndexPrev1)
             this.pos(this.value.p2, this.elementIndex)
-            this.pos(this.value.p3, this.elementIndex + 1)
-            this.pos(this.value.p4, this.elementIndex + 2)
+            this.pos(this.value.p3, elementIndexNext1)
+            this.pos(this.value.p4, elementIndexNext2)
 
             this.value.first = this.elementIndex === this.polymerSegment.start
             this.value.last = this.elementIndex === this.polymerSegment.end - 1
diff --git a/src/mol-repr/visual.ts b/src/mol-repr/visual.ts
index d7776973cc4f1fc701cec85e3e526b88431d3b4c..92072569ebe9dae974c23f7e6966aebefdafabfe 100644
--- a/src/mol-repr/visual.ts
+++ b/src/mol-repr/visual.ts
@@ -8,14 +8,17 @@ import { RuntimeContext } from 'mol-task'
 import { GraphicsRenderObject } from 'mol-gl/render-object'
 import { PickingId } from '../mol-geo/geometry/picking';
 import { Loci } from 'mol-model/loci';
-import { MarkerAction } from '../mol-geo/geometry/marker-data';
+import { MarkerAction, applyMarkerAction } from '../mol-geo/geometry/marker-data';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { WebGLContext } from 'mol-gl/webgl/context';
 import { Theme } from 'mol-theme/theme';
 import { Mat4 } from 'mol-math/linear-algebra';
-import { setTransformData } from 'mol-geo/geometry/transform-data';
+import { updateTransformData, fillIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { calculateTransformBoundingSphere } from 'mol-gl/renderable/util';
 import { ValueCell } from 'mol-util';
+import { Overpaint } from 'mol-theme/overpaint';
+import { createOverpaint, clearOverpaint, applyOverpaintColor } from 'mol-geo/geometry/overpaint-data';
+import { Interval } from 'mol-data/int';
 
 export interface VisualContext {
     readonly runtime: RuntimeContext
@@ -31,26 +34,85 @@ interface Visual<D, P extends PD.Params> {
     createOrUpdate: (ctx: VisualContext, theme: Theme, props?: Partial<PD.Values<P>>, data?: D) => Promise<void> | void
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => boolean
-    setVisibility: (value: boolean) => void
-    setPickable: (value: boolean) => void
-    setTransform: (value: Mat4) => void
+    setVisibility: (visible: boolean) => void
+    setAlphaFactor: (alphaFactor: number) => void
+    setPickable: (pickable: boolean) => void
+    setTransform: (matrix?: Mat4, instanceMatrices?: Float32Array | null) => void
+    setOverpaint: (overpaint: Overpaint) => void
     destroy: () => void
 }
 namespace Visual {
-    export function setVisibility(renderObject: GraphicsRenderObject | undefined, value: boolean) {
-        if (renderObject) renderObject.state.visible = value
+    export type LociApply = (loci: Loci, apply: (interval: Interval) => boolean) => boolean
+
+    export function setVisibility(renderObject: GraphicsRenderObject | undefined, visible: boolean) {
+        if (renderObject) renderObject.state.visible = visible
+    }
+
+    export function setAlphaFactor(renderObject: GraphicsRenderObject | undefined, alphaFactor: number) {
+        if (renderObject) renderObject.state.alphaFactor = alphaFactor
+    }
+
+    export function setPickable(renderObject: GraphicsRenderObject | undefined, pickable: boolean) {
+        if (renderObject) renderObject.state.pickable = pickable
+    }
+
+    export function mark(renderObject: GraphicsRenderObject | undefined, loci: Loci, action: MarkerAction, lociApply: LociApply) {
+        if (!renderObject) return false
+
+        const { tMarker } = renderObject.values
+
+        function apply(interval: Interval) {
+            const start = Interval.start(interval)
+            const end = Interval.end(interval)
+            return applyMarkerAction(tMarker.ref.value.array, start, end, action)
+        }
+
+        const changed = lociApply(loci, apply)
+        if (changed) ValueCell.update(tMarker, tMarker.ref.value)
+        return changed
     }
 
-    export function setPickable(renderObject: GraphicsRenderObject | undefined, value: boolean) {
-        if (renderObject) renderObject.state.pickable = value
+    export function setOverpaint(renderObject: GraphicsRenderObject | undefined, overpaint: Overpaint, lociApply: LociApply, clear: boolean) {
+        if (!renderObject) return
+
+        const { tOverpaint, uGroupCount, instanceCount } = renderObject.values
+        const count = uGroupCount.ref.value * instanceCount.ref.value
+
+        // ensure texture has right size
+        createOverpaint(overpaint.layers.length ? count : 0, renderObject.values)
+
+        // clear if requested
+        if (clear) clearOverpaint(tOverpaint.ref.value.array, 0, count)
+
+        for (let i = 0, il = overpaint.layers.length; i < il; ++i) {
+            const { loci, color } = overpaint.layers[i]
+            const apply = (interval: Interval) => {
+                const start = Interval.start(interval)
+                const end = Interval.end(interval)
+                return applyOverpaintColor(tOverpaint.ref.value.array, start, end, color, overpaint.alpha)
+            }
+            lociApply(loci, apply)
+        }
+        ValueCell.update(tOverpaint, tOverpaint.ref.value)
     }
 
-    export function setTransform(renderObject: GraphicsRenderObject | undefined, value: Mat4) {
-        if (renderObject) {
-            const { values } = renderObject
-            setTransformData(value, values)
-            const boundingSphere = calculateTransformBoundingSphere(values.invariantBoundingSphere.ref.value, values.aTransform.ref.value, values.instanceCount.ref.value)
-            ValueCell.update(values.boundingSphere, boundingSphere)
+    export function setTransform(renderObject: GraphicsRenderObject | undefined, transform?: Mat4, instanceTransforms?: Float32Array | null) {
+        if (!renderObject || (!transform && !instanceTransforms)) return
+
+        const { values } = renderObject
+        if (transform) {
+            Mat4.copy(values.matrix.ref.value, transform)
+            ValueCell.update(values.matrix, values.matrix.ref.value)
+        }
+        if (instanceTransforms) {
+            values.extraTransform.ref.value.set(instanceTransforms)
+            ValueCell.update(values.extraTransform, values.extraTransform.ref.value)
+        } else if (instanceTransforms === null) {
+            fillIdentityTransform(values.extraTransform.ref.value, values.instanceCount.ref.value)
+            ValueCell.update(values.extraTransform, values.extraTransform.ref.value)
         }
+        updateTransformData(values)
+        const boundingSphere = calculateTransformBoundingSphere(values.invariantBoundingSphere.ref.value, values.aTransform.ref.value, values.instanceCount.ref.value)
+        ValueCell.update(values.boundingSphere, boundingSphere)
     }
 }
\ No newline at end of file
diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts
index 581c35cdf37db4e110c2e59933c23233788cc562..805045ae744120de65151dbd556ee97ad3d20f04 100644
--- a/src/mol-repr/volume/direct-volume.ts
+++ b/src/mol-repr/volume/direct-volume.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,7 +7,6 @@
 import { VolumeData } from 'mol-model/volume'
 import { RuntimeContext } from 'mol-task'
 import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
-import { createRenderObject } from 'mol-gl/render-object';
 import { EmptyLoci } from 'mol-model/loci';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
@@ -15,13 +14,13 @@ import { Box3D } from 'mol-math/geometry';
 import { WebGLContext } from 'mol-gl/webgl/context';
 import { createTexture } from 'mol-gl/webgl/texture';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { BaseGeometry } from 'mol-geo/geometry/base';
 import { VisualUpdateState } from 'mol-repr/util';
 import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
 import { VisualContext } from 'mol-repr/visual';
+import { NullLocation } from 'mol-model/location';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
     const bbox = Box3D.empty()
@@ -144,7 +143,7 @@ export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, v
 
 //
 
-export async function createDirectVolume(ctx: VisualContext, volume: VolumeData, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
+export async function createDirectVolume(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<DirectVolumeParams>, directVolume?: DirectVolume) {
     const { runtime, webgl } = ctx
     if (webgl === undefined) throw new Error('DirectVolumeVisual requires `webgl` in props')
 
@@ -153,7 +152,6 @@ export async function createDirectVolume(ctx: VisualContext, volume: VolumeData,
         await createDirectVolume2d(runtime, webgl, volume, directVolume)
 }
 
-
 //
 
 export const DirectVolumeParams = {
@@ -166,22 +164,15 @@ export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: VolumeD
 }
 
 export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
-    return VolumeVisual<DirectVolumeParams>({
+    return VolumeVisual<DirectVolume, DirectVolumeParams>({
         defaultProps: PD.getDefaultValues(DirectVolumeParams),
         createGeometry: createDirectVolume,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
         getLoci: () => EmptyLoci,
-        mark: () => false,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
-        },
-        createRenderObject: (geometry: DirectVolume, locationIt: LocationIterator, theme: Theme, props: PD.Values<DirectVolumeParams>) => {
-            const transform = createIdentityTransform()
-            const values = DirectVolume.Utils.createValues(geometry, transform, locationIt, theme, props)
-            const state = DirectVolume.Utils.createRenderableState(props)
-            return createRenderObject('direct-volume', values, state)
+        eachLocation: () => false,
+        setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
         },
-        updateValues: DirectVolume.Utils.updateValues,
-        updateBoundingSphere: DirectVolume.Utils.updateBoundingSphere,
-        updateRenderableState: DirectVolume.Utils.updateRenderableState
+        geometryUtils: DirectVolume.Utils
     })
 }
 
diff --git a/src/mol-repr/volume/isosurface-mesh.ts b/src/mol-repr/volume/isosurface-mesh.ts
deleted file mode 100644
index 272d879992c4c5d12207e5874b04e96e72f547ba..0000000000000000000000000000000000000000
--- a/src/mol-repr/volume/isosurface-mesh.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Copyright (c) 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 { VolumeData } from 'mol-model/volume'
-import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
-import { createRenderObject } from 'mol-gl/render-object';
-import { EmptyLoci } from 'mol-model/loci';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Mesh } from 'mol-geo/geometry/mesh/mesh';
-import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm';
-import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
-import { VisualUpdateState } from 'mol-repr/util';
-import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
-import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
-import { VisualContext } from 'mol-repr/visual';
-
-interface VolumeIsosurfaceProps {
-    isoValue: number
-}
-
-export async function createVolumeIsosurface(ctx: VisualContext, volume: VolumeData, props: VolumeIsosurfaceProps, mesh?: Mesh) {
-    ctx.runtime.update({ message: 'Marching cubes...' });
-
-    const surface = await computeMarchingCubesMesh({
-        isoLevel: props.isoValue,
-        scalarField: volume.data
-    }, mesh).runAsChild(ctx.runtime);
-
-    const transform = VolumeData.getGridToCartesianTransform(volume);
-    ctx.runtime.update({ message: 'Transforming mesh...' });
-    Mesh.transformImmediate(surface, transform);
-    Mesh.computeNormalsImmediate(surface)
-
-    return surface;
-}
-
-export const IsosurfaceParams = {
-    ...Mesh.Params,
-    isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
-}
-export type IsosurfaceParams = typeof IsosurfaceParams
-export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
-    return PD.clone(IsosurfaceParams)
-}
-
-export function IsosurfaceVisual(): VolumeVisual<IsosurfaceParams> {
-    return VolumeVisual<IsosurfaceParams>({
-        defaultProps: PD.getDefaultValues(IsosurfaceParams),
-        createGeometry: createVolumeIsosurface,
-        getLoci: () => EmptyLoci,
-        mark: () => false,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IsosurfaceParams>, currentProps: PD.Values<IsosurfaceParams>) => {
-            if (newProps.isoValue !== currentProps.isoValue) state.createGeometry = true
-        },
-        createRenderObject: (geometry: Mesh, locationIt: LocationIterator, theme: Theme, props: PD.Values<IsosurfaceParams>) => {
-            const transform = createIdentityTransform()
-            const values = Mesh.Utils.createValues(geometry, transform, locationIt, theme, props)
-            const state = Mesh.Utils.createRenderableState(props)
-            return createRenderObject('mesh', values, state)
-        },
-        updateValues: Mesh.Utils.updateValues,
-        updateBoundingSphere: Mesh.Utils.updateBoundingSphere,
-        updateRenderableState: Mesh.Utils.updateRenderableState
-    })
-}
-
-export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceParams>): VolumeRepresentation<IsosurfaceParams> {
-    return VolumeRepresentation('Isosurface', ctx, getParams, IsosurfaceVisual)
-}
-
-export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceParams> = {
-    label: 'Isosurface',
-    description: 'Displays an isosurface of volumetric data.',
-    factory: IsosurfaceRepresentation,
-    getParams: getIsosurfaceParams,
-    defaultValues: PD.getDefaultValues(IsosurfaceParams),
-    defaultColorTheme: 'uniform',
-    defaultSizeTheme: 'uniform'
-}
\ No newline at end of file
diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b29c7afe5970b0898958696bda5c23590a15f924
--- /dev/null
+++ b/src/mol-repr/volume/isosurface.ts
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2018-2019 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 { VolumeData, VolumeIsoValue } from 'mol-model/volume'
+import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
+import { EmptyLoci } from 'mol-model/loci';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { Mesh } from 'mol-geo/geometry/mesh/mesh';
+import { computeMarchingCubesMesh, computeMarchingCubesLines } from 'mol-geo/util/marching-cubes/algorithm';
+import { LocationIterator } from 'mol-geo/util/location-iterator';
+import { VisualUpdateState } from 'mol-repr/util';
+import { RepresentationContext, RepresentationParamsGetter, Representation } from 'mol-repr/representation';
+import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
+import { VisualContext } from 'mol-repr/visual';
+import { NullLocation } from 'mol-model/location';
+import { Lines } from 'mol-geo/geometry/lines/lines';
+
+const defaultStats: VolumeData['dataStats'] = { min: -1, max: 1, mean: 0, sigma: 0.1  };
+export function createIsoValueParam(defaultValue: VolumeIsoValue, stats?: VolumeData['dataStats']) {
+    const sts = stats || defaultStats;
+    const { min, max, mean, sigma } = sts;
+
+    // using ceil/floor could lead to "ouf of bounds" when converting
+    const relMin = (min - mean) / sigma;
+    const relMax = (max - mean) / sigma;
+
+    let def = defaultValue;
+    if (defaultValue.kind === 'absolute') {
+        if (defaultValue.absoluteValue < min) def = VolumeIsoValue.absolute(min);
+        else if (defaultValue.absoluteValue > max) def = VolumeIsoValue.absolute(max);
+    } else {
+        if (defaultValue.relativeValue < relMin) def = VolumeIsoValue.relative(relMin);
+        else if (defaultValue.relativeValue > relMax) def = VolumeIsoValue.relative(relMax);
+    }
+
+    return PD.Conditioned(
+        def,
+        {
+            'absolute': PD.Converted(
+                (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats).absoluteValue,
+                (v: number) => VolumeIsoValue.absolute(v),
+                PD.Numeric(mean, { min, max, step: sigma / 100 })
+            ),
+            'relative': PD.Converted(
+                (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, VolumeData.One.dataStats).relativeValue,
+                (v: number) => VolumeIsoValue.relative(v),
+                PD.Numeric(Math.min(1, relMax), { min: relMin, max: relMax, step: Math.round(((max - min) / sigma)) / 100 })
+            )
+        },
+        (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
+        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, sts) : VolumeIsoValue.toRelative(v, sts)
+    );
+}
+
+export const IsoValueParam = createIsoValueParam(VolumeIsoValue.relative(2));
+type IsoValueParam = typeof IsoValueParam
+
+export const VolumeIsosurfaceParams = {
+    isoValue: IsoValueParam
+}
+export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
+export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
+
+//
+
+export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: VolumeData, theme: Theme, props: VolumeIsosurfaceProps, mesh?: Mesh) {
+    ctx.runtime.update({ message: 'Marching cubes...' });
+
+    const surface = await computeMarchingCubesMesh({
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
+        scalarField: volume.data
+    }, mesh).runAsChild(ctx.runtime);
+
+    const transform = VolumeData.getGridToCartesianTransform(volume);
+    ctx.runtime.update({ message: 'Transforming mesh...' });
+    Mesh.transformImmediate(surface, transform);
+    Mesh.computeNormalsImmediate(surface)
+
+    return surface;
+}
+
+export const IsosurfaceMeshParams = {
+    ...Mesh.Params,
+    ...VolumeIsosurfaceParams
+}
+export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
+
+export function IsosurfaceMeshVisual(): VolumeVisual<IsosurfaceMeshParams> {
+    return VolumeVisual<Mesh, IsosurfaceMeshParams>({
+        defaultProps: PD.getDefaultValues(IsosurfaceMeshParams),
+        createGeometry: createVolumeIsosurfaceMesh,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
+        getLoci: () => EmptyLoci,
+        eachLocation: () => false,
+        setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
+            if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true
+        },
+        geometryUtils: Mesh.Utils
+    })
+}
+
+//
+
+export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume: VolumeData, theme: Theme, props: VolumeIsosurfaceProps, lines?: Lines) {
+    ctx.runtime.update({ message: 'Marching cubes...' });
+
+    const wireframe = await computeMarchingCubesLines({
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
+        scalarField: volume.data
+    }, lines).runAsChild(ctx.runtime)
+
+    const transform = VolumeData.getGridToCartesianTransform(volume);
+    Lines.transformImmediate(wireframe, transform)
+
+    return wireframe;
+}
+
+export const IsosurfaceWireframeParams = {
+    ...Lines.Params,
+    ...VolumeIsosurfaceParams
+}
+export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams
+
+export function IsosurfaceWireframeVisual(): VolumeVisual<IsosurfaceWireframeParams> {
+    return VolumeVisual<Lines, IsosurfaceWireframeParams>({
+        defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams),
+        createGeometry: createVolumeIsosurfaceWireframe,
+        createLocationIterator: (volume: VolumeData) => LocationIterator(1, 1, () => NullLocation),
+        getLoci: () => EmptyLoci,
+        eachLocation: () => false,
+        setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<IsosurfaceWireframeParams>, currentProps: PD.Values<IsosurfaceWireframeParams>) => {
+            if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true
+        },
+        geometryUtils: Lines.Utils
+    })
+}
+
+//
+
+const IsosurfaceVisuals = {
+    'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual),
+    'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual),
+}
+type IsosurfaceVisualName = keyof typeof IsosurfaceVisuals
+const IsosurfaceVisualOptions = Object.keys(IsosurfaceVisuals).map(name => [name, name] as [IsosurfaceVisualName, string])
+
+export const IsosurfaceParams = {
+    ...IsosurfaceMeshParams,
+    ...IsosurfaceWireframeParams,
+    visuals: PD.MultiSelect<IsosurfaceVisualName>(['solid'], IsosurfaceVisualOptions),
+}
+export type IsosurfaceParams = typeof IsosurfaceParams
+export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
+    const p = PD.clone(IsosurfaceParams);
+    p.isoValue = createIsoValueParam(VolumeIsoValue.relative(2), volume.dataStats);
+    return p
+}
+
+export type IsosurfaceRepresentation = VolumeRepresentation<IsosurfaceParams>
+export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceParams>): IsosurfaceRepresentation {
+    return Representation.createMulti('Isosurface', ctx, getParams, Representation.StateBuilder, IsosurfaceVisuals as unknown as Representation.Def<VolumeData, IsosurfaceParams>)
+}
+
+export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceParams> = {
+    label: 'Isosurface',
+    description: 'Displays an isosurface of volumetric data.',
+    factory: IsosurfaceRepresentation,
+    getParams: getIsosurfaceParams,
+    defaultValues: PD.getDefaultValues(IsosurfaceParams),
+    defaultColorTheme: 'uniform',
+    defaultSizeTheme: 'uniform'
+}
\ No newline at end of file
diff --git a/src/mol-repr/volume/registry.ts b/src/mol-repr/volume/registry.ts
index 512463432e26bda845a6ec9d6b784634f8cf50f1..51507b46e7188e41f305255fcf422d6e4a780d3d 100644
--- a/src/mol-repr/volume/registry.ts
+++ b/src/mol-repr/volume/registry.ts
@@ -4,16 +4,16 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { RepresentationProvider, RepresentationRegistry } from '../representation';
+import { RepresentationProvider, RepresentationRegistry, Representation } from '../representation';
 import { VolumeData } from 'mol-model/volume';
-import { IsosurfaceRepresentationProvider } from './isosurface-mesh';
+import { IsosurfaceRepresentationProvider } from './isosurface';
 import { DirectVolumeRepresentationProvider } from './direct-volume';
 
-export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData> {
+export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData, Representation.State> {
     constructor() {
         super()
         Object.keys(BuiltInVolumeRepresentations).forEach(name => {
-            const p = (BuiltInVolumeRepresentations as { [k: string]: RepresentationProvider<VolumeData, any> })[name]
+            const p = (BuiltInVolumeRepresentations as { [k: string]: RepresentationProvider<VolumeData, any, Representation.State> })[name]
             this.add(name, p)
         })
     }
diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts
index 080463a6fca463c2bf000f49f27d190d39c37529..9b2472a4cd1a29540f927f8127ba97ffac0db454 100644
--- a/src/mol-repr/volume/representation.ts
+++ b/src/mol-repr/volume/representation.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,128 +9,182 @@ import { Representation, RepresentationContext, RepresentationProvider, Represen
 import { Visual, VisualContext } from '../visual';
 import { VolumeData } from 'mol-model/volume';
 import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
-import { Geometry } from 'mol-geo/geometry/geometry';
+import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PickingId } from 'mol-geo/geometry/picking';
-import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
-import { GraphicsRenderObject } from 'mol-gl/render-object';
+import { MarkerAction } from 'mol-geo/geometry/marker-data';
+import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object';
 import { Interval } from 'mol-data/int';
-import { RenderableValues } from 'mol-gl/renderable/schema';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { NullLocation } from 'mol-model/location';
 import { VisualUpdateState } from 'mol-repr/util';
 import { ValueCell } from 'mol-util';
 import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
-import { RenderableState } from 'mol-gl/renderable';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { BaseGeometry } from 'mol-geo/geometry/base';
+import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
+import { ColorTheme } from 'mol-theme/color';
+import { createColors } from 'mol-geo/geometry/color-data';
+import { createSizes } from 'mol-geo/geometry/size-data';
+import { Overpaint } from 'mol-theme/overpaint';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { }
 
+function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) {
+    const { createValues, createRenderableState } = Geometry.getUtils(geometry)
+    const transform = createIdentityTransform()
+    const values = createValues(geometry, transform, locationIt, theme, props)
+    const state = createRenderableState(props)
+    return createRenderObject(geometry.kind, values, state)
+}
+
 interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
     defaultProps: PD.Values<P>
-    createGeometry(ctx: VisualContext, volumeData: VolumeData, props: PD.Values<P>, geometry?: G): Promise<G>
+    createGeometry(ctx: VisualContext, volume: VolumeData, theme: Theme, props: PD.Values<P>, geometry?: G): Promise<G> | G
+    createLocationIterator(volume: VolumeData): LocationIterator
     getLoci(pickingId: PickingId, id: number): Loci
-    mark(loci: Loci, apply: (interval: Interval) => boolean): boolean
-    setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>): void
+    eachLocation(loci: Loci, apply: (interval: Interval) => boolean): boolean
+    setUpdateState(state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
 }
 
 interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry> extends VolumeVisualBuilder<P, G> {
-    createRenderObject(geometry: G, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): GraphicsRenderObject
-    updateValues(values: RenderableValues, newProps: PD.Values<P>): void,
-    updateBoundingSphere(values: RenderableValues, geometry: G): void
-    updateRenderableState(state: RenderableState, props: PD.Values<P>): void
+    geometryUtils: GeometryUtils<G>
 }
 
-export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>): VolumeVisual<P> {
-    const { defaultProps, createGeometry, getLoci, mark, setUpdateState } = builder
-    const { createRenderObject, updateValues, updateBoundingSphere, updateRenderableState } = builder
+export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>): VolumeVisual<P> {
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder
+    const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils
     const updateState = VisualUpdateState.create()
 
-    let currentProps: PD.Values<P>
     let renderObject: GraphicsRenderObject | undefined
+
+    let newProps: PD.Values<P>
+    let newTheme: Theme
+    let newVolume: VolumeData
+
+    let currentProps: PD.Values<P> = Object.assign({}, defaultProps)
+    let currentTheme: Theme = createEmptyTheme()
     let currentVolume: VolumeData
-    let geometry: Geometry
+
+    let geometry: G
     let locationIt: LocationIterator
 
-    async function create(ctx: VisualContext, volume: VolumeData, theme: Theme, props: Partial<PD.Values<P>> = {}) {
-        currentProps = Object.assign({}, defaultProps, props)
-        geometry = await createGeometry(ctx, volume, currentProps, geometry)
-        locationIt = LocationIterator(1, 1, () => NullLocation)
-        renderObject = createRenderObject(geometry, locationIt, theme, currentProps)
-    }
+    function prepareUpdate(theme: Theme, props: Partial<PD.Values<P>>, volume: VolumeData) {
+        if (!volume && !currentVolume) {
+            throw new Error('missing volume')
+        }
 
-    async function update(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}) {
-        if (!renderObject) return
-        const newProps = Object.assign({}, currentProps, props)
+        newProps = Object.assign({}, currentProps, props)
+        newTheme = theme
+        newVolume = volume
 
         VisualUpdateState.reset(updateState)
-        setUpdateState(updateState, newProps, currentProps)
+
+        if (!renderObject) {
+            updateState.createNew = true
+        } else if (!currentVolume || !VolumeData.areEquivalent(newVolume, currentVolume)) {
+            updateState.createNew = true
+        }
+
+        if (updateState.createNew) {
+            updateState.createGeometry = true
+            return
+        }
+
+        setUpdateState(updateState, volume, newProps, currentProps, newTheme, currentTheme)
+
+        if (!ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true
 
         if (updateState.createGeometry) {
-            geometry = await createGeometry(ctx, currentVolume, currentProps, geometry)
-            ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
-            updateBoundingSphere(renderObject.values, geometry)
+            updateState.updateColor = true
         }
+    }
+
+    function update(newGeometry?: G) {
+        if (updateState.createNew) {
+            locationIt = createLocationIterator(newVolume)
+            if (newGeometry) {
+                renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps)
+            } else {
+                throw new Error('expected geometry to be given')
+            }
+        } else {
+            if (!renderObject) {
+                throw new Error('expected renderObject to be available')
+            }
+
+            locationIt.reset()
+
+            if (updateState.createGeometry) {
+                if (newGeometry) {
+                    ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(newGeometry))
+                    updateBoundingSphere(renderObject.values, newGeometry)
+                } else {
+                    throw new Error('expected geometry to be given')
+                }
+            }
 
-        updateValues(renderObject.values, newProps)
-        updateRenderableState(renderObject.state, newProps)
+            if (updateState.updateSize) {
+                // not all geometries have size data, so check here
+                if ('uSize' in renderObject.values) {
+                    createSizes(locationIt, newTheme.size, renderObject.values)
+                }
+            }
+
+            if (updateState.updateColor) {
+                createColors(locationIt, newTheme.color, renderObject.values)
+            }
+
+            updateValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+        }
 
         currentProps = newProps
+        currentTheme = newTheme
+        currentVolume = newVolume
+        if (newGeometry) geometry = newGeometry
+    }
+
+    function lociApply(loci: Loci, apply: (interval: Interval) => boolean) {
+        if (isEveryLoci(loci)) {
+            return apply(Interval.ofBounds(0, locationIt.groupCount * locationIt.instanceCount))
+        } else {
+            return eachLocation(loci, apply)
+        }
     }
 
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
         get renderObject () { return renderObject },
         async createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
-            if (!volume && !currentVolume) {
-                throw new Error('missing volume')
-            } else if (volume && (!currentVolume || !renderObject)) {
-                currentVolume = volume
-                await create(ctx, volume, theme, props)
-            } else if (volume && volume !== currentVolume) {
-                currentVolume = volume
-                await create(ctx, volume, theme, props)
+            prepareUpdate(theme, props, volume || currentVolume)
+            if (updateState.createGeometry) {
+                const newGeometry = createGeometry(ctx, newVolume, newTheme, newProps, geometry)
+                return newGeometry instanceof Promise ? newGeometry.then(update) : update(newGeometry)
             } else {
-                await update(ctx, theme, props)
+                update()
             }
-
-            currentProps = Object.assign({}, defaultProps, props)
         },
         getLoci(pickingId: PickingId) {
             return renderObject ? getLoci(pickingId, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
-            if (!renderObject) return false
-            const { tMarker } = renderObject.values
-            const { groupCount, instanceCount } = locationIt
-
-            function apply(interval: Interval) {
-                const start = Interval.start(interval)
-                const end = Interval.end(interval)
-                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
-            }
-
-            let changed = false
-            if (isEveryLoci(loci)) {
-                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
-            } else {
-                changed = mark(loci, apply)
-            }
-            if (changed) {
-                ValueCell.update(tMarker, tMarker.ref.value)
-            }
-            return changed
+            return Visual.mark(renderObject, loci, action, lociApply)
+        },
+        setVisibility(visible: boolean) {
+            Visual.setVisibility(renderObject, visible)
+        },
+        setAlphaFactor(alphaFactor: number) {
+            Visual.setAlphaFactor(renderObject, alphaFactor)
         },
-        setVisibility(value: boolean) {
-            Visual.setVisibility(renderObject, value)
+        setPickable(pickable: boolean) {
+            Visual.setPickable(renderObject, pickable)
         },
-        setPickable(value: boolean) {
-            Visual.setPickable(renderObject, value)
+        setTransform(matrix?: Mat4, instanceMatrices?: Float32Array | null) {
+            Visual.setTransform(renderObject, matrix, instanceMatrices)
         },
-        setTransform(value: Mat4) {
-            Visual.setTransform(renderObject, value)
+        setOverpaint(overpaint: Overpaint) {
+            return Visual.setOverpaint(renderObject, overpaint, lociApply, true)
         },
         destroy() {
             // TODO
@@ -141,27 +195,26 @@ export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeomet
 
 export interface VolumeRepresentation<P extends VolumeParams> extends Representation<VolumeData, P> { }
 
-export type VolumeRepresentationProvider<P extends VolumeParams> = RepresentationProvider<VolumeData, P>
+export type VolumeRepresentationProvider<P extends VolumeParams> = RepresentationProvider<VolumeData, P, Representation.State>
 
 //
 
 export const VolumeParams = {
     ...BaseGeometry.Params,
-    isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
 }
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: () => VolumeVisual<P>): VolumeRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const renderObjects: GraphicsRenderObject[] = []
     const _state = Representation.createState()
-    let visual: VolumeVisual<P>
+    let visual: VolumeVisual<P> | undefined
 
     let _volume: VolumeData
-    let _props: PD.Values<P>
     let _params: P
+    let _props: PD.Values<P>
     let _theme = createEmptyTheme()
-    let busy = false
 
     function createOrUpdate(props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
         if (volume && volume !== _volume) {
@@ -171,22 +224,14 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         }
         _props = Object.assign({}, _props, props)
 
-        return Task.create('VolumeRepresentation.create', async runtime => {
-            // TODO queue it somehow
-            if (busy) return
-
-            if (!visual && !volume) {
-                throw new Error('volume data missing')
-            } else if (volume && !visual) {
-                busy = true
-                visual = visualCtor(volume)
-                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
-                busy = false
-            } else {
-                busy = true
-                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
-                busy = false
-            }
+        return Task.create('Creating or updating VolumeRepresentation', async runtime => {
+            if (!visual) visual = visualCtor()
+            const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
+            if (promise) await promise
+            // update list of renderObjects
+            renderObjects.length = 0
+            if (visual && visual.renderObject) renderObjects.push(visual.renderObject)
+            // increment version
             updated.next(version++)
         });
     }
@@ -201,7 +246,9 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
 
     function setState(state: Partial<Representation.State>) {
         if (state.visible !== undefined && visual) visual.setVisibility(state.visible)
+        if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor)
         if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
+        if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint)
         if (state.transform !== undefined && visual) visual.setTransform(state.transform)
 
         Representation.updateState(_state, state)
@@ -220,13 +267,11 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         get groupCount() {
             return visual ? visual.groupCount : 0
         },
-        get renderObjects() {
-            return visual && visual.renderObject ? [ visual.renderObject ] : []
-        },
         get props () { return _props },
         get params() { return _params },
         get state() { return _state },
         get theme() { return _theme },
+        renderObjects,
         updated,
         createOrUpdate,
         setState,
diff --git a/src/mol-script/language/parser.ts b/src/mol-script/language/parser.ts
index 906810e9d7bb4544f52863a5b70fd1a6382a8c1e..a1074abdf4f6eaa94c583537f65ebad965ad91de 100644
--- a/src/mol-script/language/parser.ts
+++ b/src/mol-script/language/parser.ts
@@ -163,7 +163,7 @@ namespace Language {
     }
 
     function isNumber(value: string) {
-        return /-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/.test(value);
+        return /-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/.test(value) && !isNaN(+value);
     }
 
     export function parse(input: string): Expression[] {
diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts
index 5ce9110e49611b5518af323c6e5896c340fe2b06..4f9f7377efb220f0dbb65717677675540095a9d1 100644
--- a/src/mol-script/language/symbol-table/structure-query.ts
+++ b/src/mol-script/language/symbol-table/structure-query.ts
@@ -150,6 +150,10 @@ const modifier = {
         'as-whole-residues': Argument(Type.Bool, { isOptional: true })
     }), Types.ElementSelectionQuery, 'Pick all atom sets that are connected to the target.'),
 
+    wholeResidues: symbol(Arguments.Dictionary({
+        0: Argument(Types.ElementSelectionQuery),
+    }), Types.ElementSelectionQuery, 'Expand the selection to whole residues.'),
+
     expandProperty: symbol(Arguments.Dictionary({
         0: Argument(Types.ElementSelectionQuery),
         property: Argument(Type.AnyValue)
@@ -242,7 +246,10 @@ const atomProperty = {
         bondCount: symbol(Arguments.Dictionary({
             0: Argument(Types.ElementReference, { isOptional: true, defaultValue: 'slot.current-atom' }),
             flags: Argument(Types.BondFlags, { isOptional: true, defaultValue: 'covalent' as any }),
-        }), Type.Num, 'Number of bonds (by default only covalent bonds are counted).')
+        }), Type.Num, 'Number of bonds (by default only covalent bonds are counted).'),
+
+        sourceIndex: atomProp(Type.Num, 'Index of the atom/element in the input file.'),
+        operatorName: atomProp(Type.Str, 'Name of the symmetry operator applied to this element.'),
     },
 
     topology: {
diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts
index 9782aeb137c4678dd5024e8db8076b34043a6f71..ba1072a587beb9c0c8ed2bf3b83dac130894b7f8 100644
--- a/src/mol-script/runtime/query/table.ts
+++ b/src/mol-script/runtime/query/table.ts
@@ -8,7 +8,7 @@ import { MolScriptSymbolTable as MolScript } from '../../language/symbol-table';
 import { DefaultQueryRuntimeTable, QuerySymbolRuntime, QueryRuntimeArguments } from './compiler';
 import { Queries, StructureProperties, StructureElement, QueryContext } from 'mol-model/structure';
 import { ElementSymbol } from 'mol-model/structure/model/types';
-import { isSuperset } from 'mol-util/set';
+import { SetUtils } from 'mol-util/set';
 import toUpperCase from 'mol-util/upper-case';
 import { VdwRadius, AtomWeight, AtomNumber } from 'mol-model/structure/model/properties/atomic';
 import { cantorPairing } from 'mol-data/util';
@@ -150,7 +150,7 @@ const symbols = [
 
     // ============= SET ================
     C(MolScript.core.set.has, (ctx, v) => v[0](ctx).has(v[1](ctx))),
-    C(MolScript.core.set.isSubset, (ctx, v) => isSuperset(v[1](ctx) as Set<any>, v[0](ctx) as Set<any>)),
+    C(MolScript.core.set.isSubset, (ctx, v) => SetUtils.isSuperset(v[1](ctx) as Set<any>, v[0](ctx) as Set<any>)),
 
     // ============= FLAGS ================
     C(MolScript.core.flags.hasAny, (ctx, v) => {
@@ -195,6 +195,25 @@ const symbols = [
         groupBy: xs['group-by']
     })(ctx)),
 
+    D(MolScript.structureQuery.generator.all, (ctx) => Queries.generators.all(ctx)),
+    D(MolScript.structureQuery.generator.empty, (ctx) => Queries.generators.none(ctx)),
+
+    // ============= MODIFIERS ================
+
+    D(MolScript.structureQuery.modifier.includeSurroundings, (ctx, xs) => Queries.modifiers.includeSurroundings(xs[0] as any, {
+        radius: xs['radius'](ctx),
+        wholeResidues: !!(xs['as-whole-residues'] && xs['as-whole-residues'](ctx)),
+        elementRadius: xs['atom-radius']
+    })(ctx)),
+    D(MolScript.structureQuery.modifier.wholeResidues, (ctx, xs) => Queries.modifiers.wholeResidues(xs[0] as any)(ctx)),
+    D(MolScript.structureQuery.modifier.union, (ctx, xs) => Queries.modifiers.union(xs[0] as any)(ctx)),
+    D(MolScript.structureQuery.modifier.expandProperty, (ctx, xs) => Queries.modifiers.expandProperty(xs[0] as any, xs['property'])(ctx)),
+    D(MolScript.structureQuery.modifier.exceptBy, (ctx, xs) => Queries.modifiers.exceptBy(xs[0] as any, xs['by'] as any)(ctx)),
+
+    // ============= COMBINATORS ================
+
+    D(MolScript.structureQuery.combinator.merge, (ctx, xs) => Queries.combinators.merge(xs as any)(ctx)),
+
     // ============= ATOM PROPERTIES ================
 
     // ~~~ CORE ~~~
@@ -205,6 +224,8 @@ const symbols = [
     D(MolScript.structureQuery.atomProperty.core.x, atomProp(StructureProperties.atom.x)),
     D(MolScript.structureQuery.atomProperty.core.y, atomProp(StructureProperties.atom.y)),
     D(MolScript.structureQuery.atomProperty.core.z, atomProp(StructureProperties.atom.z)),
+    D(MolScript.structureQuery.atomProperty.core.sourceIndex, atomProp(StructureProperties.atom.sourceIndex)),
+    D(MolScript.structureQuery.atomProperty.core.operatorName, atomProp(StructureProperties.unit.operator_name)),
     D(MolScript.structureQuery.atomProperty.core.atomKey, (ctx, _) => cantorPairing(ctx.element.unit.id, ctx.element.element)),
 
     // TODO:
diff --git a/src/mol-script/script/mol-script/symbols.ts b/src/mol-script/script/mol-script/symbols.ts
index 0e2f2a741d8c859c1a74e8ab9c9016475192e1e9..0c4826f7ffc685e85a84b9617b83188def6a6e06 100644
--- a/src/mol-script/script/mol-script/symbols.ts
+++ b/src/mol-script/script/mol-script/symbols.ts
@@ -7,7 +7,6 @@
 import { UniqueArray } from 'mol-data/generic';
 import Expression from '../../language/expression';
 import { Argument, MSymbol } from '../../language/symbol';
-//import * as M from './macro'
 import { MolScriptSymbolTable as MolScript } from '../../language/symbol-table';
 import Type from '../../language/type';
 
@@ -196,6 +195,8 @@ export const SymbolTable = [
             Alias(MolScript.structureQuery.atomProperty.core.x, 'atom.x'),
             Alias(MolScript.structureQuery.atomProperty.core.y, 'atom.y'),
             Alias(MolScript.structureQuery.atomProperty.core.z, 'atom.z'),
+            Alias(MolScript.structureQuery.atomProperty.core.sourceIndex, 'atom.src-index'),
+            Alias(MolScript.structureQuery.atomProperty.core.operatorName, 'atom.op-name'),
             Alias(MolScript.structureQuery.atomProperty.core.atomKey, 'atom.key'),
             Alias(MolScript.structureQuery.atomProperty.core.bondCount, 'atom.bond-count'),
 
@@ -342,4 +343,4 @@ export function transpileMolScript(expr: Expression) {
 //     if (a.length === b.length) return (a < b) as any;
 //     return a.length - b.length;
 // });
-//export default [...sortedSymbols, ...NamedArgs.map(a => ':' + a), ...Constants];
\ No newline at end of file
+// export default [...sortedSymbols, ...NamedArgs.map(a => ':' + a), ...Constants];
\ No newline at end of file
diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts
index 1b178246fbc2fe31d421b4c116db744492117813..81baf5fc5e3f34971c159c1cfce116cf98e5694b 100644
--- a/src/mol-state/action.ts
+++ b/src/mol-state/action.ts
@@ -9,7 +9,8 @@ import { UUID } from 'mol-util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
-import { Transformer } from './transformer';
+import { StateTransformer } from './transformer';
+import { StateTransform } from './transform';
 
 export { StateAction };
 
@@ -45,7 +46,7 @@ namespace StateAction {
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
         /** Test if the transform can be applied to a given node */
-        isApplicable?(a: A, globalCtx: unknown): boolean
+        isApplicable?(a: A, aTransform: StateTransform<StateTransformer<any, A, any>>, globalCtx: unknown): boolean
     }
 
     export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
@@ -63,15 +64,18 @@ namespace StateAction {
         return action;
     }
 
-    export function fromTransformer<T extends Transformer>(transformer: T) {
+    export function fromTransformer<T extends StateTransformer>(transformer: T) {
         const def = transformer.definition;
-        return create<Transformer.From<T>, void, Transformer.Params<T>>({
+        return create<StateTransformer.From<T>, void, StateTransformer.Params<T>>({
             from: def.from,
             display: def.display,
-            params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'],
+            params: def.params as StateTransformer.Definition<StateTransformer.From<T>, any, StateTransformer.Params<T>>['params'],
+            isApplicable: transformer.definition.isApplicable
+                ? (a, t, ctx) => transformer.definition.isApplicable!(a, ctx)
+                : void 0,
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
-                return state.update(tree);
+                return state.updateTree(tree) as Task<void>;
             }
         })
     }
@@ -80,7 +84,8 @@ namespace StateAction {
         export interface Type<A extends StateObject.Ctor, P extends { }> {
             from?: A | A[],
             params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
-            display?: string | { name: string, description?: string }
+            display?: string | { name: string, description?: string },
+            isApplicable?: DefinitionBase<StateObject.From<A>, any, P>['isApplicable']
         }
 
         export interface Root {
@@ -106,6 +111,7 @@ namespace StateAction {
                     : !!info.params
                     ? info.params as any
                     : void 0,
+                isApplicable: info.isApplicable,
                 ...(typeof def === 'function'
                     ? { run: def }
                     : def)
diff --git a/src/mol-state/action/manager.ts b/src/mol-state/action/manager.ts
index 4e489df48ccf19d884881f97fd7152a730062276..5a2f87b79cba66bc166831cb4d0a6bb805333759 100644
--- a/src/mol-state/action/manager.ts
+++ b/src/mol-state/action/manager.ts
@@ -4,9 +4,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateAction } from 'mol-state/action';
-import { StateObject } from '../object';
-import { Transformer } from 'mol-state/transformer';
+import { StateAction } from '../action';
+import { StateObject, StateObjectCell } from '../object';
+import { StateTransformer } from '../transformer';
 
 export { StateActionManager }
 
@@ -14,8 +14,8 @@ class StateActionManager {
     private actions: Map<StateAction['id'], StateAction> = new Map();
     private fromTypeIndex = new Map<StateObject.Type, StateAction[]>();
 
-    add(actionOrTransformer: StateAction | Transformer) {
-        const action = Transformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer;
+    add(actionOrTransformer: StateAction | StateTransformer) {
+        const action = StateTransformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer;
 
         if (this.actions.has(action.id)) return this;
 
@@ -32,7 +32,31 @@ class StateActionManager {
         return this;
     }
 
-    fromType(type: StateObject.Type): ReadonlyArray<StateAction> {
-        return this.fromTypeIndex.get(type) || [];
+    fromCell(cell: StateObjectCell, ctx: unknown): ReadonlyArray<StateAction> {
+        const obj = cell.obj;
+        if (!obj) return [];
+
+        const actions = this.fromTypeIndex.get(obj.type);
+        if (!actions) return [];
+        let hasTest = false;
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                hasTest = true;
+                break;
+            }
+        }
+        if (!hasTest) return actions;
+
+        const ret: StateAction[] = [];
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                if (a.definition.isApplicable(obj, cell.transform, ctx)) {
+                    ret.push(a);
+                }
+            } else {
+                ret.push(a);
+            }
+        }
+        return ret;
     }
 }
\ No newline at end of file
diff --git a/src/mol-state/index.ts b/src/mol-state/index.ts
index 8ef37d2fd8463422cf850a1f5aafb69b448ac16c..5d8d630f6180271f2aa218fb35b5d4f391fdc3b9 100644
--- a/src/mol-state/index.ts
+++ b/src/mol-state/index.ts
@@ -5,7 +5,10 @@
  */
 
 export * from './object'
+export * from './tree'
 export * from './state'
+export * from './state/builder'
+export * from './state/selection'
 export * from './transformer'
-export * from './tree'
-export * from './transform'
\ No newline at end of file
+export * from './transform'
+export * from './action'
\ No newline at end of file
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index a2cdfbb0f5a5df67f6df1f191e915dfa3f23d668..b74efcf5ee78f98572acf08c549929d32282932d 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -5,8 +5,10 @@
  */
 
 import { UUID } from 'mol-util';
-import { Transform } from './transform';
+import { StateTransform } from './transform';
 import { ParamDefinition } from 'mol-util/param-definition';
+import { State } from './state';
+import { StateSelection, StateTransformer } from 'mol-state';
 
 export { StateObject, StateObjectCell }
 
@@ -16,6 +18,8 @@ interface StateObject<D = any, T extends StateObject.Type = StateObject.Type<any
     readonly data: D,
     readonly label: string,
     readonly description?: string,
+    // assigned by reconciler to be StateTransform.props.tag
+    readonly tag?: string
 }
 
 namespace StateObject {
@@ -51,13 +55,12 @@ namespace StateObject {
     };
 }
 
-interface StateObjectCell {
-    transform: Transform,
+interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> {
+    transform: F,
 
     // Which object was used as a parent to create data in this cell
-    sourceRef: Transform.Ref | undefined,
+    sourceRef: StateTransform.Ref | undefined,
 
-    version: string
     status: StateObjectCell.Status,
 
     params: {
@@ -66,12 +69,17 @@ interface StateObjectCell {
     } | undefined;
 
     errorText?: string,
-    obj?: StateObject
+    obj?: T,
+
+    cache: unknown | undefined
 }
 
 namespace StateObjectCell {
     export type Status = 'ok' | 'error' | 'pending' | 'processing'
 
+    export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never
+    export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never
+
     export interface State {
         isHidden: boolean,
         isCollapsed: boolean
@@ -89,4 +97,28 @@ namespace StateObjectCell {
         if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true;
         return false;
     }
+}
+
+// TODO: improve the API?
+export class StateObjectTracker<T extends StateObject> {
+    private query: StateSelection.Query;
+    private version: string = '';
+    cell: StateObjectCell | undefined;
+    data: T['data'] | undefined;
+
+    setQuery(sel: StateSelection.Selector) {
+        this.query = StateSelection.compile(sel);
+    }
+
+    update() {
+        const cell = this.state.select(this.query)[0];
+        const version = cell ? cell.transform.version : void 0;
+        const changed = this.cell !== cell || this.version !== version;
+        this.cell = cell;
+        this.version = version || '';
+        this.data = cell && cell.obj ? cell.obj.data as T : void 0 as any;
+        return changed;
+    }
+
+    constructor(private state: State) { }
 }
\ No newline at end of file
diff --git a/src/mol-state/reducer.ts b/src/mol-state/reducer.ts
deleted file mode 100644
index 839dbddc6661da8f2ff5107266ad38c8c4ba7ad9..0000000000000000000000000000000000000000
--- a/src/mol-state/reducer.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// TODO: state reducers
-// The idea is to have "reduced"/computed objects from a state
-// basicaly something like this Reduce(selection: StateSelection) => Value
-// the reducer would be automatically called each time a state update is finished and any object of the selection is updated.
\ No newline at end of file
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index 9efabe8107c2970be0e9cec8d87be17892976e35..66563660ade1dd87bd95a428fd867f2d20618baa 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -1,41 +1,41 @@
 /**
- * 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 { StateObject, StateObjectCell } from './object';
 import { StateTree } from './tree';
-import { Transform } from './transform';
-import { Transformer } from './transformer';
-import { UUID } from 'mol-util';
+import { StateTransform } from './transform';
+import { StateTransformer } from './transformer';
 import { RuntimeContext, Task } from 'mol-task';
 import { StateSelection } from './state/selection';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
-import { StateTreeBuilder } from './tree/builder';
+import { StateBuilder } from './state/builder';
 import { StateAction } from './action';
 import { StateActionManager } from './action/manager';
 import { TransientTree } from './tree/transient';
 import { LogEntry } from 'mol-util/log-entry';
 import { now, formatTimespan } from 'mol-util/now';
 import { ParamDefinition } from 'mol-util/param-definition';
+import { StateTreeSpine } from './tree/spine';
+import { AsyncQueue } from 'mol-util/async-queue';
 
 export { State }
 
 class State {
-    private _tree: TransientTree = StateTree.createEmpty().asTransient();
+    private _tree: TransientTree;
 
     protected errorFree = true;
-    private transformCache = new Map<Transform.Ref, unknown>();
 
     private ev = RxEventHelper.create();
 
     readonly globalContext: unknown = void 0;
     readonly events = {
         cell: {
-            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State}>(),
+            stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
-            removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(),
+            removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
         object: {
             updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
@@ -43,11 +43,12 @@ class State {
             removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
         },
         log: this.ev<LogEntry>(),
-        changed: this.ev<void>()
+        changed: this.ev<void>(),
+        isUpdating: this.ev<boolean>()
     };
 
     readonly behaviors = {
-        currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef })
+        currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: StateTransform.RootRef })
     };
 
     readonly actions = new StateActionManager();
@@ -57,9 +58,10 @@ class State {
     get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
-    build() { return this._tree.build(); }
+    build() { return new StateBuilder.Root(this.tree, this); }
 
     readonly cells: State.Cells = new Map();
+    private spine = new StateTreeSpine.Impl(this.cells);
 
     getSnapshot(): State.Snapshot {
         return { tree: StateTree.toJSON(this._tree) };
@@ -67,14 +69,14 @@ class State {
 
     setSnapshot(snapshot: State.Snapshot) {
         const tree = StateTree.fromJSON(snapshot.tree);
-        return this.update(tree);
+        return this.updateTree(tree);
     }
 
-    setCurrent(ref: Transform.Ref) {
+    setCurrent(ref: StateTransform.Ref) {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
+    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
         const update = typeof stateOrProvider === 'function'
             ? stateOrProvider(this.tree.cellStates.get(ref))
             : stateOrProvider;
@@ -89,17 +91,28 @@ class State {
     }
 
     /**
-     * Select Cells by ref or a query generated on the fly.
-     * @example state.select('test')
+     * Select Cells using the provided selector.
+     * @example state.query(StateSelection.Generators.byRef('test').ancestorOfType([type]))
+     * @example state.query('test')
+     */
+    select<C extends StateObjectCell>(selector: StateSelection.Selector<C>) {
+        return StateSelection.select(selector, this)
+    }
+
+    /**
+     * Select Cells by building a query generated on the fly.
      * @example state.select(q => q.byRef('test').subtree())
      */
-    select(selector: Transform.Ref | ((q: typeof StateSelection.Generators) => StateSelection.Selector)) {
+    selectQ<C extends StateObjectCell>(selector: (q: typeof StateSelection.Generators) => StateSelection.Selector<C>) {
         if (typeof selector === 'string') return StateSelection.select(selector, this);
         return StateSelection.select(selector(StateSelection.Generators), this)
     }
 
-    /** If no ref is specified, apply to root */
-    apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
+    /**
+     * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
+     * If no ref is specified, apply to root.
+     */
+    applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: StateTransform.Ref = StateTransform.RootRef): Task<void> {
         return Task.create('Apply Action', ctx => {
             const cell = this.cells.get(ref);
             if (!cell) throw new Error(`'${ref}' does not exist.`);
@@ -109,56 +122,103 @@ class State {
         });
     }
 
-    update(tree: StateTree | StateTreeBuilder): Task<void> {
-        const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
+    /**
+     * Queues up a reconciliation of the existing state tree.
+     *
+     * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task.
+     * @param tree Tree instance or a tree builder instance
+     * @param doNotReportTiming Indicates whether to log timing of the individual transforms
+     */
+    updateTree<T extends StateObject>(tree: StateBuilder.To<T>, options?: Partial<State.UpdateOptions>): Task<T>
+    updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void>
+    updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> {
+        const params: UpdateParams = { tree, options };
         return Task.create('Update Tree', async taskCtx => {
-            let updated = false;
+            const removed = await this.updateQueue.enqueue(params);
+            if (!removed) return;
+
             try {
-                const oldTree = this._tree;
-                this._tree = _tree;
-
-                const ctx: UpdateContext = {
-                    parent: this,
-                    editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
-
-                    errorFree: this.errorFree,
-                    taskCtx,
-                    oldTree,
-                    tree: _tree,
-                    cells: this.cells as Map<Transform.Ref, StateObjectCell>,
-                    transformCache: this.transformCache,
-
-                    results: [],
-
-                    changed: false,
-                    hadError: false,
-                    newCurrent: void 0
-                };
-
-                this.errorFree = true;
-                // TODO: handle "cancelled" error? Or would this be handled automatically?
-                updated = await update(ctx);
+                const ret = await this._updateTree(taskCtx, params);
+                return ret;
             } finally {
-                if (updated) this.events.changed.next();
+                this.updateQueue.handled(params);
             }
+        }, () => {
+            this.updateQueue.remove(params);
         });
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
+    private updateQueue = new AsyncQueue<UpdateParams>();
+
+    private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) {
+        this.events.isUpdating.next(true);
+        let updated = false;
+        const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options);
+        try {
+            updated = await update(ctx);
+            if (StateBuilder.isTo(params.tree)) {
+                const cell = this.select(params.tree.ref)[0];
+                return cell && cell.obj;
+            }
+        } finally {
+            this.spine.setSurrent();
+
+            if (updated) this.events.changed.next();
+            this.events.isUpdating.next(false);
+
+            for (const ref of ctx.stateChanges) {
+                this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
+            }
+        }
+    }
+
+    private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, options: Partial<State.UpdateOptions> | undefined) {
+        const _tree = (StateBuilder.is(tree) ? tree.getTree() : tree).asTransient();
+        const oldTree = this._tree;
+        this._tree = _tree;
+
+        const ctx: UpdateContext = {
+            parent: this,
+            editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0,
+
+            errorFree: this.errorFree,
+            taskCtx,
+            oldTree,
+            tree: _tree,
+            cells: this.cells as Map<StateTransform.Ref, StateObjectCell>,
+            spine: this.spine,
+
+            results: [],
+            stateChanges: [],
+
+            options: { ...StateUpdateDefaultOptions, ...options },
+
+            changed: false,
+            hadError: false,
+            newCurrent: void 0
+        };
+
+        this.errorFree = true;
+
+        return ctx;
+    }
+
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
+        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient();
         const tree = this._tree;
         const root = tree.root;
 
-        (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, {
+        (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
             transform: root,
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
-            version: root.version,
             errorText: void 0,
             params: {
-                definition: { },
-                values: { }
-            }
+                definition: {},
+                values: {}
+            },
+            cache: { }
         });
 
         this.globalContext = params && params.globalContext;
@@ -166,10 +226,10 @@ class State {
 }
 
 namespace State {
-    export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell>
+    export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell>
 
     export type Tree = StateTree
-    export type Builder = StateTreeBuilder
+    export type Builder = StateBuilder
 
     export interface ObjectEvent {
         state: State,
@@ -180,25 +240,41 @@ namespace State {
         readonly tree: StateTree.Serialized
     }
 
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
+    export interface UpdateOptions {
+        doNotLogTiming: boolean,
+        doNotUpdateCurrent: boolean
+    }
+
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
         return new State(rootObject, params);
     }
 }
 
-type Ref = Transform.Ref
+const StateUpdateDefaultOptions: State.UpdateOptions = {
+    doNotLogTiming: false,
+    doNotUpdateCurrent: false
+};
+
+type Ref = StateTransform.Ref
+
+type UpdateParams = { tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions> }
 
 interface UpdateContext {
     parent: State,
-    editInfo: StateTreeBuilder.EditInfo | undefined
+    editInfo: StateBuilder.EditInfo | undefined
 
     errorFree: boolean,
     taskCtx: RuntimeContext,
     oldTree: StateTree,
     tree: TransientTree,
-    cells: Map<Transform.Ref, StateObjectCell>,
-    transformCache: Map<Ref, unknown>,
+    cells: Map<StateTransform.Ref, StateObjectCell>,
+    spine: StateTreeSpine.Impl,
 
     results: UpdateNodeResult[],
+    stateChanges: StateTransform.Ref[],
+
+    // suppress timing messages
+    options: State.UpdateOptions,
 
     changed: boolean,
     hadError: boolean,
@@ -209,7 +285,7 @@ async function update(ctx: UpdateContext) {
     // if only a single node was added/updated, we can skip potentially expensive diffing
     const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree);
 
-    let deletes: Transform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: Transform.Ref[];
+    let deletes: StateTransform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: StateTransform.Ref[];
 
     if (fastTrack) {
         deletes = [];
@@ -236,7 +312,6 @@ async function update(ctx: UpdateContext) {
         for (const d of deletes) {
             const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
             ctx.cells.delete(d);
-            ctx.transformCache.delete(d);
             deletedObjects.push(obj);
         }
 
@@ -244,15 +319,16 @@ async function update(ctx: UpdateContext) {
         roots = findUpdateRoots(ctx.cells, ctx.tree);
     }
 
+    let newCellStates: StateTree.CellStates;
+    if (!ctx.editInfo) {
+        newCellStates = ctx.tree.cellStatesSnapshot();
+        syncOldStates(ctx);
+    }
+
     // Init empty cells where not present
     // this is done in "pre order", meaning that "parents" will be created 1st.
     const addedCells = initCells(ctx, roots);
 
-    // Ensure cell states stay consistent
-    if (!ctx.editInfo) {
-        syncStates(ctx);
-    }
-
     // Notify additions of new cells.
     for (const cell of addedCells) {
         ctx.parent.events.cell.created.next({ state: ctx.parent, ref: cell.transform.ref, cell });
@@ -275,7 +351,12 @@ async function update(ctx: UpdateContext) {
         await updateSubtree(ctx, root);
     }
 
-    let newCurrent: Transform.Ref | undefined = ctx.newCurrent;
+    // Sync cell states
+    if (!ctx.editInfo) {
+        syncNewStates(ctx, newCellStates!);
+    }
+
+    let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
     // Raise object updated events
     for (const update of ctx.results) {
         if (update.action === 'created') {
@@ -291,32 +372,31 @@ async function update(ctx: UpdateContext) {
         }
     }
 
-    if (newCurrent) ctx.parent.setCurrent(newCurrent);
-    else {
+    if (newCurrent) {
+        if (!ctx.options.doNotUpdateCurrent) ctx.parent.setCurrent(newCurrent);
+    } else {
         // check if old current or its parent hasn't become null
         const current = ctx.parent.current;
         const currentCell = ctx.cells.get(current);
-        if (currentCell && (
-                currentCell.obj === StateObject.Null
+        if (currentCell && (currentCell.obj === StateObject.Null
             || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) {
             newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells);
             ctx.parent.setCurrent(newCurrent);
         }
     }
 
-
     return deletes.length > 0 || roots.length > 0 || ctx.changed;
 }
 
-function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) {
+function findUpdateRoots(cells: Map<StateTransform.Ref, StateObjectCell>, tree: StateTree) {
     const findState = { roots: [] as Ref[], cells };
     StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
     return findState.roots;
 }
 
-function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
+function findUpdateRootsVisitor(n: StateTransform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
     const cell = s.cells.get(n.ref);
-    if (!cell || cell.version !== n.version || cell.status === 'error') {
+    if (!cell || cell.transform.version !== n.version || cell.status === 'error') {
         s.roots.push(n.ref);
         return false;
     }
@@ -326,7 +406,7 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells:
 }
 
 type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
-function checkDeleteVisitor(n: Transform, _: any, ctx: FindDeletesCtx) {
+function checkDeleteVisitor(n: StateTransform, _: any, ctx: FindDeletesCtx) {
     if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
 }
 function findDeletes(ctx: UpdateContext): Ref[] {
@@ -335,12 +415,25 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
 }
 
-function syncStatesVisitor(n: Transform, tree: StateTree, oldState: StateTree.CellStates) {
-    if (!oldState.has(n.ref)) return;
-    (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
+function syncOldStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) {
+    if (oldState.has(n.ref)) {
+        (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
+    }
+}
+function syncOldStates(ctx: UpdateContext) {
+    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncOldStatesVisitor);
+}
+
+function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: { newState: StateTree.CellStates, changes: StateTransform.Ref[] }) {
+    if (ctx.newState.has(n.ref)) {
+        const changed = (tree as TransientTree).updateCellState(n.ref, ctx.newState.get(n.ref));
+        if (changed) {
+            ctx.changes.push(n.ref);
+        }
+    }
 }
-function syncStates(ctx: UpdateContext) {
-    StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncStatesVisitor);
+function syncNewStates(ctx: UpdateContext, newState: StateTree.CellStates) {
+    StateTree.doPreOrder(ctx.tree, ctx.tree.root, { newState, changes: ctx.stateChanges }, syncNewStatesVisitor);
 }
 
 function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
@@ -351,7 +444,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
     if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
 }
 
-function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) {
+function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
     ctx.cells.get(t.ref)!.transform = t;
     setCellStatus(ctx, t.ref, 'pending');
 }
@@ -363,7 +456,7 @@ function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
 }
 
 type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] }
-function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCellsCtx) {
+function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: InitCellsCtx) {
     if (ctx.cells.has(transform.ref)) {
         return;
     }
@@ -372,9 +465,9 @@ function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCell
         transform,
         sourceRef: void 0,
         status: 'pending',
-        version: UUID.create22(),
         errorText: void 0,
-        params: void 0
+        params: void 0,
+        cache: void 0
     };
     ctx.cells.set(transform.ref, cell);
     added.push(cell);
@@ -394,7 +487,7 @@ function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<
 }
 
 function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref {
-    if (ref === Transform.RootRef) return ref;
+    if (ref === StateTransform.RootRef) return ref;
 
     const node = tree.transforms.get(ref)!;
     const siblings = tree.children.get(node.parent)!.values();
@@ -447,8 +540,8 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined, si
     if (cell.obj) {
         const obj = cell.obj;
         cell.obj = void 0;
+        cell.cache = void 0;
         ctx.parent.events.object.removed.next({ state: ctx.parent, ref, obj });
-        ctx.transformCache.delete(ref);
     }
 
     // remove the objects in the child nodes if they exist
@@ -483,18 +576,19 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
         ctx.results.push(update);
         if (update.action === 'created') {
             isNull = update.obj === StateObject.Null;
-            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
         } else if (update.action === 'updated') {
             isNull = update.obj === StateObject.Null;
-            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
         } else if (update.action === 'replaced') {
             isNull = update.obj === StateObject.Null;
-            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
         }
     } catch (e) {
         ctx.changed = true;
         if (!ctx.hadError) ctx.newCurrent = root;
         doError(ctx, root, '' + e, false);
+        console.error(e);
         return;
     }
 
@@ -507,9 +601,9 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
     }
 }
 
-function resolveParams(ctx: UpdateContext, transform: Transform, src: StateObject) {
+function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) {
     const prms = transform.transformer.definition.params;
-    const definition = prms ? prms(src, ctx.parent.globalContext) : { };
+    const definition = prms ? prms(src, ctx.parent.globalContext) : {};
     const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
     return { definition, values };
 }
@@ -520,16 +614,19 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
     const transform = current.transform;
 
     // special case for Root
-    if (current.transform.ref === Transform.RootRef) {
-        current.version = transform.version;
+    if (current.transform.ref === StateTransform.RootRef) {
         return { action: 'none' };
     }
 
-    const parentCell = StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
+    let parentCell = transform.transformer.definition.from.length === 0
+    ? ctx.cells.get(current.transform.parent)
+    : StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
     if (!parentCell) {
         throw new Error(`No suitable parent found for '${currentRef}'`);
     }
 
+    ctx.spine.setSurrent(current);
+
     const parent = parentCell.obj!;
     current.sourceRef = parentCell.transform.ref;
 
@@ -537,9 +634,9 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
     if (!oldTree.transforms.has(currentRef) || !current.params) {
         current.params = params;
-        const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values);
+        const obj = await createObject(ctx, current, transform.transformer, parent, params.values);
+        updateTag(obj, transform);
         current.obj = obj;
-        current.version = transform.version;
 
         return { ref: currentRef, action: 'created', obj };
     } else {
@@ -548,46 +645,49 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
         current.params = params;
 
         const updateKind = !!current.obj && current.obj !== StateObject.Null
-            ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams)
-            : Transformer.UpdateResult.Recreate;
+            ? await updateObject(ctx, current, transform.transformer, parent, current.obj!, oldParams, newParams)
+            : StateTransformer.UpdateResult.Recreate;
 
         switch (updateKind) {
-            case Transformer.UpdateResult.Recreate: {
+            case StateTransformer.UpdateResult.Recreate: {
                 const oldObj = current.obj;
-                const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams);
+                const newObj = await createObject(ctx, current, transform.transformer, parent, newParams);
+                updateTag(newObj, transform);
                 current.obj = newObj;
-                current.version = transform.version;
                 return { ref: currentRef, action: 'replaced', oldObj, obj: newObj };
             }
-            case Transformer.UpdateResult.Updated:
-                current.version = transform.version;
+            case StateTransformer.UpdateResult.Updated:
+                updateTag(current.obj, transform);
+                return { ref: currentRef, action: 'updated', obj: current.obj! };
+            case StateTransformer.UpdateResult.Null: {
+                current.obj = StateObject.Null;
                 return { ref: currentRef, action: 'updated', obj: current.obj! };
+            }
             default:
-                current.version = transform.version;
                 return { action: 'none' };
         }
     }
 }
 
+function updateTag(obj: StateObject | undefined, transform: StateTransform) {
+    if (!obj || obj === StateObject.Null) return;
+    (obj.tag as string | undefined) = transform.props.tag;
+}
+
 function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
     if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx);
     return t as T;
 }
 
-function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
-    const cache = Object.create(null);
-    ctx.transformCache.set(ref, cache);
-    return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx);
+function createObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, params: any) {
+    if (!cell.cache) cell.cache = Object.create(null);
+    return runTask(transformer.definition.apply({ a, params, cache: cell.cache, spine: ctx.spine }, ctx.parent.globalContext), ctx.taskCtx);
 }
 
-async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
+async function updateObject(ctx: UpdateContext, cell: StateObjectCell,  transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
     if (!transformer.definition.update) {
-        return Transformer.UpdateResult.Recreate;
-    }
-    let cache = ctx.transformCache.get(ref);
-    if (!cache) {
-        cache = Object.create(null);
-        ctx.transformCache.set(ref, cache);
+        return StateTransformer.UpdateResult.Recreate;
     }
-    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.parent.globalContext), ctx.taskCtx);
+    if (!cell.cache) cell.cache = Object.create(null);
+    return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache, spine: ctx.spine }, ctx.parent.globalContext), ctx.taskCtx);
 }
\ No newline at end of file
diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e9e48a10a680457d27f4efa86e602a0450f032f
--- /dev/null
+++ b/src/mol-state/state/builder.ts
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTree } from '../tree/immutable';
+import { TransientTree } from '../tree/transient';
+import { StateObject, StateObjectCell } from '../object';
+import { StateTransform } from '../transform';
+import { StateTransformer } from '../transformer';
+import { State } from 'mol-state/state';
+
+export { StateBuilder }
+
+interface StateBuilder {
+    readonly editInfo: StateBuilder.EditInfo,
+    getTree(): StateTree
+}
+
+namespace StateBuilder {
+    export interface EditInfo {
+        sourceTree: StateTree,
+        count: number,
+        lastUpdate?: StateTransform.Ref
+    }
+
+    interface BuildState {
+        state: State | undefined,
+        tree: TransientTree,
+        editInfo: EditInfo,
+        actions: Action[]
+    }
+
+    type Action =
+        | { kind: 'add', transform: StateTransform }
+        | { kind: 'update', ref: string, params: any }
+        | { kind: 'delete', ref: string }
+        | { kind: 'insert', ref: string, transform: StateTransform, initialCellState?: Partial<StateObjectCell.State> }
+
+    function buildTree(state: BuildState) {
+        if (!state.state || state.state.tree === state.editInfo.sourceTree) {
+            return state.tree.asImmutable();
+        }
+
+        // The tree has changed in the meantime, we need to reapply the changes!
+        const tree = state.state.tree.asTransient();
+        for (const a of state.actions) {
+            switch (a.kind) {
+                case 'add': tree.add(a.transform); break;
+                case 'update': tree.setParams(a.ref, a.params); break;
+                case 'delete': tree.remove(a.ref); break;
+                case 'insert': {
+                    const children = tree.children.get(a.ref).toArray();
+                    tree.add(a.transform, a.initialCellState);
+                    for (const c of children) {
+                        tree.changeParent(c, a.transform.ref);
+                    }
+                    break;
+                }
+            }
+        }
+        state.editInfo.sourceTree = state.tree;
+        return tree.asImmutable();
+    }
+
+    export function is(obj: any): obj is StateBuilder {
+        return !!obj && typeof (obj as StateBuilder).getTree === 'function';
+    }
+
+    export function isTo(obj: any): obj is StateBuilder.To<any> {
+        return !!obj && typeof (obj as StateBuilder).getTree === 'function' && typeof (obj as StateBuilder.To<any>).ref === 'string';
+    }
+
+    export class Root implements StateBuilder {
+        private state: BuildState;
+        get editInfo() { return this.state.editInfo; }
+
+        to<A extends StateObject>(ref: StateTransform.Ref): To<A>
+        to<C extends StateObjectCell>(cell: C): To<StateObjectCell.Obj<C>, StateTransform.Transformer<StateObjectCell.Transform<C>>>
+        to(refOrCell: StateTransform.Ref | StateObjectCell) {
+            const ref = typeof refOrCell === 'string' ? refOrCell : refOrCell.transform.ref;
+            return new To<StateObject, StateTransformer>(this.state, ref, this);
+        }
+        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
+        delete(ref: StateTransform.Ref) {
+            this.editInfo.count++;
+            this.state.tree.remove(ref);
+            this.state.actions.push({ kind: 'delete', ref });
+            return this;
+        }
+        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+        constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
+    }
+
+    export class To<A extends StateObject, T extends StateTransformer = StateTransformer> implements StateBuilder {
+        get editInfo() { return this.state.editInfo; }
+
+        readonly ref: StateTransform.Ref;
+
+        /**
+         * Apply the transformed to the parent node
+         * If no params are specified (params <- undefined), default params are lazily resolved.
+         */
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+            const t = tr.apply(this.ref, params, options);
+            this.state.tree.add(t, initialCellState);
+            this.editInfo.count++;
+            this.editInfo.lastUpdate = t.ref;
+
+            this.state.actions.push({ kind: 'add', transform: t });
+
+            return new To(this.state, t.ref, this.root);
+        }
+
+        /**
+         * A helper to greate a group-like state object and keep the current type.
+         */
+        group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<A> {
+            return this.apply(tr, params, options, initialCellState) as To<A>;
+        }
+
+        /**
+         * Inserts a new transform that does not change the object type and move the original children to it.
+         */
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+            // cache the children
+            const children = this.state.tree.children.get(this.ref).toArray();
+
+            // add the new node
+            const t = tr.apply(this.ref, params, options);
+            this.state.tree.add(t, initialCellState);
+
+            // move the original children to the new node
+            for (const c of children) {
+                this.state.tree.changeParent(c, t.ref);
+            }
+
+            this.editInfo.count++;
+            this.editInfo.lastUpdate = t.ref;
+
+            this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t, initialCellState });
+
+            return new To(this.state, t.ref, this.root);
+        }
+
+        // /**
+        //  * Updates a transform in an instantiated tree, passing the transform's source into the providers
+        //  *
+        //  * This only works if the transform source is NOT updated by the builder. Use at own discression.
+        //  */
+        // updateInState<T extends StateTransformer<any, A, any>>(transformer: T, state: State, provider: (old: StateTransformer.Params<T>, a: StateTransformer.From<T>) => StateTransformer.Params<T>): Root {
+        //     const old = this.state.tree.transforms.get(this.ref)!;
+        //     const cell = state.cells.get(this.ref);
+        //     if (!cell || !cell.sourceRef) throw new Error('Source cell is not present in the tree.');
+        //     const parent = state.cells.get(cell.sourceRef);
+        //     if (!parent || !parent.obj) throw new Error('Parent cell is not present or computed.');
+
+        //     const params = provider(old.params as any, parent.obj as any);
+
+        //     if (this.state.tree.setParams(this.ref, params)) {
+        //         this.editInfo.count++;
+        //         this.editInfo.lastUpdate = this.ref;
+        //     }
+
+        //     return this.root;
+        // }
+
+        update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>): Root
+        update(params: StateTransformer.Params<T> | ((old: StateTransformer.Params<T>) => StateTransformer.Params<T>)): Root
+        update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T | any, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) {
+            let params: any;
+            if (provider) {
+                const old = this.state.tree.transforms.get(this.ref)!;
+                params = provider(old.params as any);
+            } else {
+                params = typeof paramsOrTransformer === 'function'
+                    ? paramsOrTransformer(this.state.tree.transforms.get(this.ref)!.params)
+                    : paramsOrTransformer;
+            }
+
+            if (this.state.tree.setParams(this.ref, params)) {
+                this.editInfo.count++;
+                this.editInfo.lastUpdate = this.ref;
+            }
+
+            this.state.actions.push({ kind: 'update', ref: this.ref, params });
+
+            return this.root;
+        }
+
+        to<A extends StateObject>(ref: StateTransform.Ref) { return this.root.to<A>(ref); }
+        toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
+        delete(ref: StateTransform.Ref) { return this.root.delete(ref); }
+
+        getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); }
+
+        constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) {
+            this.ref = ref;
+            if (!this.state.tree.transforms.has(ref)) {
+                throw new Error(`Could not find node '${ref}'.`);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts
index d25a82835478a7ea909a0f6b30e63d2203f9b7a0..12a31907342c71fbfcdb7dc27ac750e255034e7f 100644
--- a/src/mol-state/state/selection.ts
+++ b/src/mol-state/state/selection.ts
@@ -7,29 +7,30 @@
 import { StateObject, StateObjectCell } from '../object';
 import { State } from '../state';
 import { StateTree } from '../tree';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
+import { StateTransformer } from '../transformer';
 
 namespace StateSelection {
-    export type Selector = Query | Builder | string | StateObjectCell;
-    export type CellSeq = StateObjectCell[]
-    export type Query = (state: State) => CellSeq;
+    export type Selector<C extends StateObjectCell = StateObjectCell> = Query<C> | Builder<C> | string | C;
+    export type CellSeq<C extends StateObjectCell = StateObjectCell> = C[]
+    export type Query<C extends StateObjectCell = StateObjectCell> = (state: State) => CellSeq<C>;
 
-    export function select(s: Selector, state: State) {
+    export function select<C extends StateObjectCell>(s: Selector<C>, state: State) {
         return compile(s)(state);
     }
 
-    export function compile(s: Selector): Query {
+    export function compile<C extends StateObjectCell>(s: Selector<C>): Query<C> {
         const selector = s ? s : Generators.root;
         let query: Query;
         if (isBuilder(selector)) query = (selector as any).compile();
         else if (isObj(selector)) query = (Generators.byValue(selector) as any).compile();
         else if (isQuery(selector)) query = selector;
         else query = (Generators.byRef(selector as string) as any).compile();
-        return query;
+        return query as Query<C>;
     }
 
     function isObj(arg: any): arg is StateObjectCell {
-        return (arg as StateObjectCell).version !== void 0;
+        return (arg as StateObjectCell).transform !== void 0 && (arg as StateObjectCell).status !== void 0;
     }
 
     function isBuilder(arg: any): arg is Builder {
@@ -40,21 +41,23 @@ namespace StateSelection {
         return typeof arg === 'function';
     }
 
-    export interface Builder {
-        flatMap(f: (n: StateObjectCell) => StateObjectCell[]): Builder;
-        mapEntity(f: (n: StateObjectCell) => StateObjectCell): Builder;
-        unique(): Builder;
+    export interface Builder<C extends StateObjectCell = StateObjectCell> {
+        flatMap<D extends StateObjectCell>(f: (n: C) => D[]): Builder<D>;
+        mapObject<D extends StateObjectCell>(f: (n: C) => D): Builder<D>;
+        unique(): Builder<C>;
 
-        parent(): Builder;
-        first(): Builder;
-        filter(p: (n: StateObjectCell) => boolean): Builder;
-        withStatus(s: StateObjectCell.Status): Builder;
+        parent(): Builder<C>;
+        first(): Builder<C>;
+        filter(p: (n: C) => boolean): Builder<C>;
+        withTransformer<T extends StateTransformer<any, StateObjectCell.Obj<C>, any>>(t: T): Builder<StateObjectCell<StateObjectCell.Obj<C>, StateTransform<T>>>;
+        withStatus(s: StateObjectCell.Status): Builder<C>;
         subtree(): Builder;
         children(): Builder;
-        ofType(t: StateObject.Ctor): Builder;
-        ancestorOfType(t: StateObject.Ctor): Builder;
+        ofType<T extends StateObject.Ctor>(t: T): Builder<StateObjectCell<StateObject.From<T>>>;
+        ancestorOfType<T extends StateObject.Ctor>(t: T[]): Builder<StateObjectCell<StateObject.From<T>>>;
+        rootOfType(t: StateObject.Ctor[]): Builder;
 
-        select(state: State): CellSeq
+        select(state: State): CellSeq<C>
     }
 
     const BuilderPrototype: any = {
@@ -67,36 +70,52 @@ namespace StateSelection {
         BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) };
     }
 
-    function build(compile: () => Query): Builder {
+    function build<C extends StateObjectCell>(compile: () => Query<C>): Builder<C> {
         return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } });
     }
 
     export namespace Generators {
         export const root = build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]);
 
-        export function byRef(...refs: Transform.Ref[]) {
+        export function byRef<T extends StateObject.Ctor>(...refs: StateTransform.Ref[]) {
             return build(() => (state: State) => {
-                const ret: StateObjectCell[] = [];
+                const ret: StateObjectCell<StateObject.From<T>>[] = [];
                 for (const ref of refs) {
                     const n = state.cells.get(ref);
                     if (!n) continue;
-                    ret.push(n);
+                    ret.push(n as any);
                 }
                 return ret;
             });
         }
 
-        export function byValue(...objects: StateObjectCell[]) { return build(() => (state: State) => objects); }
+        export function byValue<T extends StateObjectCell>(...objects: T[]) { return build(() => (state: State) => objects); }
 
-        export function rootsOfType(type: StateObject.Ctor) {
+        export function rootsOfType<T extends StateObject.Ctor>(type: T, root: StateTransform.Ref = StateTransform.RootRef) {
             return build(() => state => {
-                const ctx = { roots: [] as StateObjectCell[], cells: state.cells, type: type.type };
-                StateTree.doPreOrder(state.tree, state.tree.root, ctx, _findRootsOfType);
+                const ctx = { roots: [] as StateObjectCell<StateObject.From<T>>[], cells: state.cells, type: type.type };
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findRootsOfType);
                 return ctx.roots;
             });
         }
 
-        function _findRootsOfType(n: Transform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) {
+        export function ofType<T extends StateObject.Ctor>(type: T, root: StateTransform.Ref = StateTransform.RootRef) {
+            return build(() => state => {
+                const ctx = { ret: [] as StateObjectCell<StateObject.From<T>>[], cells: state.cells, type: type.type };
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findOfType);
+                return ctx.ret;
+            });
+        }
+
+        export function ofTransformer<T extends StateTransformer<any, A, any>, A extends StateObject>(t: T, root: StateTransform.Ref = StateTransform.RootRef) {
+            return build(() => state => {
+                const ctx = { ret: [] as StateObjectCell<A, StateTransform<T>>[], cells: state.cells, t };
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findOfTransformer);
+                return ctx.ret;
+            });
+        }
+
+        function _findRootsOfType(n: StateTransform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) {
             const cell = s.cells.get(n.ref);
             if (cell && cell.obj && cell.obj.type === s.type) {
                 s.roots.push(cell);
@@ -104,6 +123,22 @@ namespace StateSelection {
             }
             return true;
         }
+
+        function _findOfType(n: StateTransform, _: any, s: { type: StateObject.Type, ret: StateObjectCell[], cells: State.Cells }) {
+            const cell = s.cells.get(n.ref);
+            if (cell && cell.obj && cell.obj.type === s.type) {
+                s.ret.push(cell);
+            }
+            return true;
+        }
+
+        function _findOfTransformer(n: StateTransform, _: any, s: { t: StateTransformer, ret: StateObjectCell[], cells: State.Cells }) {
+            const cell = s.cells.get(n.ref);
+            if (cell && cell.obj && cell.transform.transformer === s.t) {
+                s.ret.push(cell);
+            }
+            return true;
+        }
     }
 
     registerModifier('flatMap', flatMap);
@@ -120,8 +155,8 @@ namespace StateSelection {
         });
     }
 
-    registerModifier('mapEntity', mapEntity);
-    export function mapEntity(b: Selector, f: (n: StateObjectCell, state: State) => StateObjectCell | undefined) {
+    registerModifier('mapObject', mapObject);
+    export function mapObject(b: Selector, f: (n: StateObjectCell, state: State) => StateObjectCell | undefined) {
         const q = compile(b);
         return build(() => (state: State) => {
             const ret: StateObjectCell[] = [];
@@ -187,12 +222,18 @@ namespace StateSelection {
     export function ofType(b: Selector, t: StateObject.Ctor) { return filter(b, n => n.obj ? n.obj.type === t.type : false); }
 
     registerModifier('ancestorOfType', ancestorOfType);
-    export function ancestorOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s.tree, s.cells, n.transform.ref, types))); }
+    export function ancestorOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapObject(b, (n, s) => findAncestorOfType(s.tree, s.cells, n.transform.ref, types))); }
+
+    registerModifier('withTransformer', withTransformer);
+    export function withTransformer(b: Selector, t: StateTransformer) { return filter(b, o => o.transform.transformer === t); }
+
+    registerModifier('rootOfType', rootOfType);
+    export function rootOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapObject(b, (n, s) => findRootOfType(s.tree, s.cells, n.transform.ref, types))); }
 
     registerModifier('parent', parent);
-    export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); }
+    export function parent(b: Selector) { return unique(mapObject(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); }
 
-    export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
+    export function findAncestorOfType<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: T[]): StateObjectCell<StateObject.From<T>> | undefined {
         let current = tree.transforms.get(root)!, len = types.length;
         while (true) {
             current = tree.transforms.get(current.parent)!;
@@ -200,13 +241,50 @@ namespace StateSelection {
             if (!cell.obj) return void 0;
             const obj = cell.obj;
             for (let i = 0; i < len; i++) {
-                if (obj.type === types[i].type) return cells.get(current.ref);
+                if (obj.type === types[i].type) return cell as StateObjectCell<StateObject.From<T>>;
             }
-            if (current.ref === Transform.RootRef) {
+            if (current.ref === StateTransform.RootRef) {
                 return void 0;
             }
         }
     }
+
+    export function findRootOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
+        let parent: StateObjectCell | undefined, _root = root;
+        while (true) {
+            const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types);
+            if (_parent) {
+                parent = _parent;
+                _root = _parent.transform.ref;
+            } else {
+                break;
+            }
+        }
+        return parent;
+    }
+
+    export function findUniqueTagsInSubtree<K extends string = string>(tree: StateTree, root: StateTransform.Ref, tags: Set<K>): { [name in K]?: StateTransform.Ref } {
+        return StateTree.doPreOrder(tree, tree.transforms.get(root), { refs: { }, tags }, _findUniqueTagsInSubtree).refs;
+    }
+
+    function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) {
+        if (n.props.tag && s.tags.has(n.props.tag)) {
+            s.refs[n.props.tag] = n.ref;
+        }
+        return true;
+    }
+
+    export function findTagInSubtree(tree: StateTree, root: StateTransform.Ref, tag: string): StateTransform.Ref | undefined {
+        return StateTree.doPreOrder(tree, tree.transforms.get(root), { ref: void 0, tag }, _findTagInSubtree).ref;
+    }
+
+    function _findTagInSubtree(n: StateTransform, _: any, s: { ref: string | undefined, tag: string }) {
+        if (n.props.tag === s.tag) {
+            s.ref = n.ref;
+            return false;
+        }
+        return true;
+    }
 }
 
 export { StateSelection }
\ No newline at end of file
diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts
index 86edad5a9e06d2fd28e0ed74c76b1a4085c046a6..9703a2f53254c6dbfbcf2e5449b8a0775679cc05 100644
--- a/src/mol-state/transform.ts
+++ b/src/mol-state/transform.ts
@@ -4,28 +4,31 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateObject } from './object';
-import { Transformer } from './transformer';
+import { StateTransformer } from './transformer';
 import { UUID } from 'mol-util';
 
-export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
+export { Transform as StateTransform }
+
+interface Transform<T extends StateTransformer = StateTransformer> {
     readonly parent: Transform.Ref,
-    readonly transformer: Transformer<A, B, P>,
+    readonly transformer: T,
     readonly props: Transform.Props,
     readonly ref: Transform.Ref,
-    readonly params?: P,
+    readonly params?: StateTransformer.Params<T>,
     readonly version: string
 }
 
-export namespace Transform {
+namespace Transform {
     export type Ref = string
+    export type Transformer<T extends Transform> = T extends Transform<infer S> ? S : never
 
     export const RootRef = '-=root=-' as Ref;
 
     export interface Props {
         tag?: string
         isGhost?: boolean,
-        isBinding?: boolean
+        // determine if the corresponding cell can be deleted by the user.
+        isLocked?: boolean
     }
 
     export interface Options {
@@ -33,7 +36,7 @@ export namespace Transform {
         props?: Props
     }
 
-    export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
+    export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
         return {
             parent,
@@ -45,12 +48,20 @@ export namespace Transform {
         }
     }
 
-    export function withParams<T>(t: Transform, params: any): Transform {
+    export function withParams(t: Transform, params: any): Transform {
         return { ...t, params, version: UUID.create22() };
     }
 
-    export function createRoot(): Transform {
-        return create(RootRef, Transformer.ROOT, {}, { ref: RootRef });
+    export function withParent(t: Transform, parent: Ref): Transform {
+        return { ...t, parent, version: UUID.create22() };
+    }
+
+    export function withNewVersion(t: Transform): Transform {
+        return { ...t, version: UUID.create22() };
+    }
+
+    export function createRoot(props?: Props): Transform {
+        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props });
     }
 
     export interface Serialized {
@@ -78,7 +89,7 @@ export namespace Transform {
     }
 
     export function fromJSON(t: Serialized): Transform {
-        const transformer = Transformer.get(t.transformer);
+        const transformer = StateTransformer.get(t.transformer);
         const pFromJson = transformer.definition.customSerialization
             ? transformer.definition.customSerialization.toJSON
             : _id;
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 05663bd8555f5da3b8ddcbaf23c0fd881acd5e74..01759e7382efe88f25f3311f1509300c06290ced 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -5,25 +5,29 @@
  */
 
 import { Task } from 'mol-task';
-import { StateObject } from './object';
-import { Transform } from './transform';
+import { StateObject, StateObjectCell } from './object';
+import { StateTransform } from './transform';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateAction } from './action';
 import { capitalize } from 'mol-util/string';
+import { StateTreeSpine } from './tree/spine';
 
-export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
-    apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
+export { Transformer as StateTransformer }
+
+interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = any> {
+    apply(parent: StateTransform.Ref, params?: P, props?: Partial<StateTransform.Options>): StateTransform<this>,
     toAction(): StateAction<A, void, P>,
     readonly namespace: string,
     readonly id: Transformer.Id,
     readonly definition: Transformer.Definition<A, B, P>
 }
 
-export namespace Transformer {
+namespace Transformer {
     export type Id = string & { '@type': 'transformer-id' }
     export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
     export type From<T extends Transformer<any, any, any>> = T extends Transformer<infer A, any, any> ? A : unknown;
     export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
+    export type Cell<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? StateObjectCell<B> : unknown;
 
     export function is(obj: any): obj is Transformer {
         return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function';
@@ -33,7 +37,8 @@ export namespace Transformer {
         a: A,
         params: P,
         /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
-        cache: unknown
+        cache: unknown,
+        spine: StateTreeSpine
     }
 
     export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
@@ -42,7 +47,8 @@ export namespace Transformer {
         oldParams: P,
         newParams: P,
         /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */
-        cache: unknown
+        cache: unknown,
+        spine: StateTreeSpine
     }
 
     export interface AutoUpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
@@ -52,7 +58,7 @@ export namespace Transformer {
         newParams: P
     }
 
-    export enum UpdateResult { Unchanged, Updated, Recreate }
+    export enum UpdateResult { Unchanged, Updated, Recreate, Null }
 
     /** Specify default control descriptors for the parameters */
     // export type ParamsDefinition<A extends StateObject = StateObject, P = any> = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any }
@@ -89,7 +95,7 @@ export namespace Transformer {
         readonly from: StateObject.Ctor[],
         readonly to: StateObject.Ctor[],
         readonly display: { readonly name: string, readonly description?: string },
-        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
+        params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any },
     }
 
     const registry = new Map<Id, Transformer<any, any>>();
@@ -105,6 +111,10 @@ export namespace Transformer {
         }
     }
 
+    export function getAll() {
+        return Array.from(registry.values());
+    }
+
     export function get(id: string): Transformer {
         const t = registry.get(id as Id);
         if (!t) {
@@ -126,7 +136,7 @@ export namespace Transformer {
         }
 
         const t: Transformer<A, B, P> = {
-            apply(parent, params, props) { return Transform.create<A, B, P>(parent, t, params, props); },
+            apply(parent, params, props) { return StateTransform.create<Transformer<A, B, P>>(parent, t, params, props); },
             toAction() { return StateAction.fromTransformer(t); },
             namespace,
             id,
@@ -151,7 +161,8 @@ export namespace Transformer {
             name: string,
             from: A | A[],
             to: B | B[],
-            params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
+            /** The source StateObject can be undefined: used for generating docs. */
+            params?: PD.For<P> | ((a: StateObject.From<A> | undefined, globalCtx: any) => PD.For<P>),
             display?: string | { name: string, description?: string }
         }
 
@@ -195,7 +206,7 @@ export namespace Transformer {
         name: 'root',
         from: [],
         to: [],
-        display: { name: 'Root' },
+        display: { name: 'Root', description: 'For internal use.' },
         apply() { throw new Error('should never be applied'); },
         update() { return UpdateResult.Unchanged; }
     })
diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts
deleted file mode 100644
index 2eb22efd86b5797c1845f75603646ca94af8ff79..0000000000000000000000000000000000000000
--- a/src/mol-state/tree/builder.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { StateTree } from './immutable';
-import { TransientTree } from './transient';
-import { StateObject, StateObjectCell } from '../object';
-import { Transform } from '../transform';
-import { Transformer } from '../transformer';
-
-export { StateTreeBuilder }
-
-interface StateTreeBuilder {
-    readonly editInfo: StateTreeBuilder.EditInfo,
-    getTree(): StateTree
-}
-
-namespace StateTreeBuilder {
-    export interface EditInfo {
-        sourceTree: StateTree,
-        count: number,
-        lastUpdate?: Transform.Ref
-    }
-
-    interface State {
-        tree: TransientTree,
-        editInfo: EditInfo
-    }
-
-    export function is(obj: any): obj is StateTreeBuilder {
-        return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function';
-    }
-
-    export class Root implements StateTreeBuilder {
-        private state: State;
-        get editInfo() { return this.state.editInfo; }
-
-        to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
-        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
-        delete(ref: Transform.Ref) {
-            this.editInfo.count++;
-            this.state.tree.remove(ref);
-            return this;
-        }
-        getTree(): StateTree { return this.state.tree.asImmutable(); }
-        constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
-    }
-
-    export class To<A extends StateObject> implements StateTreeBuilder {
-        get editInfo() { return this.state.editInfo; }
-
-        readonly ref: Transform.Ref;
-
-        /**
-         * Apply the transformed to the parent node
-         * If no params are specified (params <- undefined), default params are lazily resolved.
-         */
-        apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, options?: Partial<Transform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<Transformer.To<T>> {
-            const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
-            this.editInfo.count++;
-            this.editInfo.lastUpdate = t.ref;
-            return new To(this.state, t.ref, this.root);
-        }
-
-        update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root
-        update(params: any): Root
-        update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) {
-            let params: any;
-            if (provider) {
-                const old = this.state.tree.transforms.get(this.ref)!;
-                params = provider(old.params as any);
-            } else {
-                params = paramsOrTransformer;
-            }
-
-            if (this.state.tree.setParams(this.ref, params)) {
-                this.editInfo.count++;
-                this.editInfo.lastUpdate = this.ref;
-            }
-
-            return this.root;
-        }
-
-        to<A extends StateObject>(ref: Transform.Ref) { return this.root.to<A>(ref); }
-        toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
-        delete(ref: Transform.Ref) { return this.root.delete(ref); }
-
-        getTree(): StateTree { return this.state.tree.asImmutable(); }
-
-        constructor(private state: State, ref: Transform.Ref, private root: Root) {
-            this.ref = ref;
-            if (!this.state.tree.transforms.has(ref)) {
-                throw new Error(`Could not find node '${ref}'.`);
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts
index 8d77221c278c662fd785604d64a8e3093084d3a6..ab3d8f60301414dc4deb67d41348a393eea6e2f5 100644
--- a/src/mol-state/tree/immutable.ts
+++ b/src/mol-state/tree/immutable.ts
@@ -5,9 +5,8 @@
  */
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 
 export { StateTree }
@@ -17,17 +16,16 @@ export { StateTree }
  * Represented as an immutable map.
  */
 interface StateTree {
-    readonly root: Transform,
+    readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
     readonly cellStates: StateTree.CellStates,
 
-    asTransient(): TransientTree,
-    build(): StateTreeBuilder.Root
+    asTransient(): TransientTree
 }
 
 namespace StateTree {
-    type Ref = Transform.Ref
+    type Ref = StateTransform.Ref
 
     export interface ChildSet {
         readonly size: number,
@@ -43,21 +41,17 @@ namespace StateTree {
         get(ref: Ref): T
     }
 
-    export interface Transforms extends _Map<Transform> {}
+    export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
     export interface CellStates extends _Map<StateObjectCell.State> { }
 
     class Impl implements StateTree {
-        get root() { return this.transforms.get(Transform.RootRef)! }
+        get root() { return this.transforms.get(StateTransform.RootRef)! }
 
         asTransient(): TransientTree {
             return new TransientTree(this);
         }
 
-        build(): StateTreeBuilder.Root {
-            return new StateTreeBuilder.Root(this);
-        }
-
         constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
         }
     }
@@ -65,8 +59,8 @@ namespace StateTree {
     /**
      * Create an instance of an immutable tree.
      */
-    export function createEmpty(): StateTree {
-        const root = Transform.createRoot();
+    export function createEmpty(customRoot?: StateTransform): StateTree {
+        const root = customRoot || StateTransform.createRoot();
         return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]]));
     }
 
@@ -74,10 +68,10 @@ namespace StateTree {
         return new Impl(nodes, children, cellStates);
     }
 
-    type VisitorCtx = { tree: StateTree, state: any, f: (node: Transform, tree: StateTree, state: any) => boolean | undefined | void };
+    type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void };
 
     function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.transforms.get(c!)!); }
-    function _doPostOrder(ctx: VisitorCtx, root: Transform) {
+    function _doPostOrder(ctx: VisitorCtx, root: StateTransform) {
         const children = ctx.tree.children.get(root.ref);
         if (children && children.size) {
             children.forEach(_postOrderFunc, ctx);
@@ -88,14 +82,14 @@ namespace StateTree {
     /**
      * Visit all nodes in a subtree in "post order", meaning leafs get visited first.
      */
-    export function doPostOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S {
+    export function doPostOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S {
         const ctx: VisitorCtx = { tree, state, f };
         _doPostOrder(ctx, root);
         return ctx.state;
     }
 
     function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); }
-    function _doPreOrder(ctx: VisitorCtx, root: Transform) {
+    function _doPreOrder(ctx: VisitorCtx, root: StateTransform) {
         const ret = ctx.f(root, ctx.tree, ctx.state);
         if (typeof ret === 'boolean' && !ret) return;
         const children = ctx.tree.children.get(root.ref);
@@ -108,44 +102,44 @@ namespace StateTree {
      * Visit all nodes in a subtree in "pre order", meaning leafs get visited last.
      * If the visitor function returns false, the visiting for that branch is interrupted.
      */
-    export function doPreOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S {
+    export function doPreOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S {
         const ctx: VisitorCtx = { tree, state, f };
         _doPreOrder(ctx, root);
         return ctx.state;
     }
 
-    function _subtree(n: Transform, _: any, subtree: Transform[]) { subtree.push(n); }
+    function _subtree(n: StateTransform, _: any, subtree: StateTransform[]) { subtree.push(n); }
     /**
      * Get all nodes in a subtree, leafs come first.
      */
-    export function subtreePostOrder<T>(tree: StateTree, root: Transform) {
-        return doPostOrder<Transform[]>(tree, root, [], _subtree);
+    export function subtreePostOrder(tree: StateTree, root: StateTransform) {
+        return doPostOrder<StateTransform[]>(tree, root, [], _subtree);
     }
 
-    function _visitNodeToJson(node: Transform, tree: StateTree, ctx: [Transform.Serialized, StateObjectCell.State][]) {
+    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) {
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
-        ctx.push([Transform.toJSON(node), tree.cellStates.get(node.ref)]);
+        ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]);
     }
 
     export interface Serialized {
         /** Transforms serialized in pre-order */
-        transforms: [Transform.Serialized, StateObjectCell.State][]
+        transforms: [StateTransform.Serialized, StateObjectCell.State][]
     }
 
-    export function toJSON<T>(tree: StateTree): Serialized {
-        const transforms: [Transform.Serialized, StateObjectCell.State][] = [];
+    export function toJSON(tree: StateTree): Serialized {
+        const transforms: [StateTransform.Serialized, StateObjectCell.State][] = [];
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
     }
 
-    export function fromJSON<T>(data: Serialized): StateTree {
-        const nodes = ImmutableMap<Ref, Transform>().asMutable();
+    export function fromJSON(data: Serialized): StateTree {
+        const nodes = ImmutableMap<Ref, StateTransform>().asMutable();
         const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
         const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
         for (const t of data.transforms) {
-            const transform = Transform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t[0]);
             nodes.set(transform.ref, transform);
             cellStates.set(transform.ref, t[1]);
 
diff --git a/src/mol-state/tree/spine.ts b/src/mol-state/tree/spine.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e30a8b276607438ef0da4e2baadd32f813576785
--- /dev/null
+++ b/src/mol-state/tree/spine.ts
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { State } from '../state';
+import { StateTransform } from '../transform';
+import { StateObject, StateObjectCell } from '../object';
+
+export { StateTreeSpine }
+
+/** The tree spine allows access to ancestor of a node during reconciliation. */
+interface StateTreeSpine {
+    getAncestorOfType<T extends StateObject.Ctor>(type: T): StateObject.From<T> | undefined;
+    getRootOfType<T extends StateObject.Ctor>(type: T): StateObject.From<T> | undefined;
+}
+
+namespace StateTreeSpine {
+    export class Impl implements StateTreeSpine {
+        private current: StateObjectCell | undefined = void 0;
+        setSurrent(cell?: StateObjectCell) {
+            this.current = cell;
+        }
+
+        getAncestorOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
+            if (!this.current) return void 0;
+            let cell = this.current;
+            while (true) {
+                cell = this.cells.get(cell.transform.parent)!;
+                if (!cell.obj) return void 0;
+                if (cell.obj.type === t.type) return cell.obj as StateObject.From<T>;
+                if (cell.transform.ref === StateTransform.RootRef) return void 0;
+            }
+        }
+
+        getRootOfType<T extends StateObject.Ctor>(t: T): StateObject.From<T> | undefined {
+            if (!this.current) return void 0;
+            let cell = this.current;
+            let ret: StateObjectCell | undefined = void 0;
+            while (true) {
+                cell = this.cells.get(cell.transform.parent)!;
+                if (!cell.obj) return void 0;
+                if (cell.obj.type === t.type) {
+                    ret = cell;
+                }
+                if (cell.transform.ref === StateTransform.RootRef) return ret ? ret.obj as StateObject.From<T> : void 0;
+            }
+        }
+
+        constructor(private cells: State.Cells) {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts
index 9646dab8f430b2322fc5e2c83138c976019b835c..a48e8e6635778e3037298890dedf69f5f892f1b8 100644
--- a/src/mol-state/tree/transient.ts
+++ b/src/mol-state/tree/transient.ts
@@ -5,24 +5,23 @@
  */
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 
 export { TransientTree }
 
 class TransientTree implements StateTree {
-    transforms = this.tree.transforms as ImmutableMap<Transform.Ref, Transform>;
-    children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>;
-    cellStates = this.tree.cellStates as ImmutableMap<Transform.Ref, StateObjectCell.State>;
+    transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
+    children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
+    cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>;
 
     private changedNodes = false;
     private changedChildren = false;
     private changedStates = false;
 
-    private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0;
+    private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
 
     private get childMutations() {
         if (this._childMutations) return this._childMutations;
@@ -48,36 +47,36 @@ class TransientTree implements StateTree {
         this.children = this.children.asMutable();
     }
 
-    get root() { return this.transforms.get(Transform.RootRef)! }
+    get root() { return this.transforms.get(StateTransform.RootRef)! }
 
-    build(): StateTreeBuilder.Root {
-        return new StateTreeBuilder.Root(this);
+    cellStatesSnapshot() {
+        return this.cellStates.asImmutable();
     }
 
     asTransient() {
         return this.asImmutable().asTransient();
     }
 
-    private addChild(parent: Transform.Ref, child: Transform.Ref) {
+    private addChild(parent: StateTransform.Ref, child: StateTransform.Ref) {
         this.changeChildren();
 
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.add(child);
         } else {
-            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
+            const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable();
             set.add(child);
             this.children.set(parent, set);
             this.childMutations.set(parent, set);
         }
     }
 
-    private removeChild(parent: Transform.Ref, child: Transform.Ref) {
+    private removeChild(parent: StateTransform.Ref, child: StateTransform.Ref) {
         this.changeChildren();
 
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.remove(child);
         } else {
-            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
+            const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable();
             set.remove(child);
             this.children.set(parent, set);
             this.childMutations.set(parent, set);
@@ -85,17 +84,35 @@ class TransientTree implements StateTree {
     }
 
     private clearRoot() {
-        const parent = Transform.RootRef;
+        const parent = StateTransform.RootRef;
         if (this.children.get(parent).size === 0) return;
 
         this.changeChildren();
 
-        const set = OrderedSet<Transform.Ref>();
+        const set = OrderedSet<StateTransform.Ref>();
         this.children.set(parent, set);
         this.childMutations.set(parent, set);
     }
 
-    add(transform: Transform, initialState?: Partial<StateObjectCell.State>) {
+    changeParent(ref: StateTransform.Ref, newParent: StateTransform.Ref) {
+        ensurePresent(this.transforms, ref);
+
+        const old = this.transforms.get(ref);
+        this.removeChild(old.parent, ref);
+        this.addChild(newParent, ref);
+        this.changeNodes();
+        this.transforms.set(ref, StateTransform.withParent(old, newParent));
+    }
+
+    updateVersion(ref: StateTransform.Ref) {
+        ensurePresent(this.transforms, ref);
+
+        const t = this.transforms.get(ref);
+        this.changeNodes();
+        this.transforms.set(ref, StateTransform.withNewVersion(t));
+    }
+
+    add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) {
         const ref = transform.ref;
 
         if (this.transforms.has(transform.ref)) {
@@ -134,7 +151,7 @@ class TransientTree implements StateTree {
     }
 
     /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */
-    setParams(ref: Transform.Ref, params: any) {
+    setParams(ref: StateTransform.Ref, params: any) {
         ensurePresent(this.transforms, ref);
 
         const transform = this.transforms.get(ref)!;
@@ -148,11 +165,11 @@ class TransientTree implements StateTree {
             this.transforms = this.transforms.asMutable();
         }
 
-        this.transforms.set(transform.ref, Transform.withParams(transform, params));
+        this.transforms.set(transform.ref, StateTransform.withParams(transform, params));
         return true;
     }
 
-    updateCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) {
+    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
         ensurePresent(this.transforms, ref);
 
         const old = this.cellStates.get(ref);
@@ -164,12 +181,12 @@ class TransientTree implements StateTree {
         return true;
     }
 
-    remove(ref: Transform.Ref): Transform[] {
+    remove(ref: StateTransform.Ref): StateTransform[] {
         const node = this.transforms.get(ref);
         if (!node) return [];
 
         const st = StateTree.subtreePostOrder(this, node);
-        if (ref === Transform.RootRef) {
+        if (ref === StateTransform.RootRef) {
             st.pop();
             if (st.length === 0) return st;
             this.clearRoot();
@@ -206,17 +223,17 @@ class TransientTree implements StateTree {
     }
 }
 
-function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
+function fixChildMutations(this: ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>, m: OrderedSet<StateTransform.Ref>, k: StateTransform.Ref) { this.set(k, m.asImmutable()); }
 
-function alreadyPresent(ref: Transform.Ref) {
+function alreadyPresent(ref: StateTransform.Ref) {
     throw new Error(`Transform '${ref}' is already present in the tree.`);
 }
 
-function parentNotPresent(ref: Transform.Ref) {
+function parentNotPresent(ref: StateTransform.Ref) {
     throw new Error(`Parent '${ref}' must be present in the tree.`);
 }
 
-function ensurePresent(nodes: StateTree.Transforms, ref: Transform.Ref) {
+function ensurePresent(nodes: StateTree.Transforms, ref: StateTransform.Ref) {
     if (!nodes.has(ref)) {
         throw new Error(`Node '${ref}' is not present in the tree.`);
     }
diff --git a/src/mol-task/util/user-timing.ts b/src/mol-task/util/user-timing.ts
index fc64b57abf1f62ce6563f0bbb2e5920f2dc90c56..a4e3b7e33808f2dad86b1c59c929a85dda08a550 100644
--- a/src/mol-task/util/user-timing.ts
+++ b/src/mol-task/util/user-timing.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,17 +7,23 @@
 import { Task } from '../task'
 
 const hasPerformance = typeof performance !== 'undefined'
+/**
+ * on node `process.env.NODE_ENV` is available, in webpack build it is automatically set
+ * by the DefinePlugin to the webpack `mode` value
+ */
+const productionMode = process.env.NODE_ENV === 'production'
+const timingEnabled = hasPerformance && !productionMode
 
 export namespace UserTiming {
     function startMarkName(task: Task<any>) { return `startTask${task.id}` }
     function endMarkName(task: Task<any>) { return `endTask${task.id}` }
     export function markStart(task: Task<any>) {
-        if (hasPerformance) performance.mark(startMarkName(task))
+        if (timingEnabled) performance.mark(startMarkName(task))
     }
     export function markEnd(task: Task<any>) {
-        if (hasPerformance) performance.mark(endMarkName(task))
+        if (timingEnabled) performance.mark(endMarkName(task))
     }
     export function measure(task: Task<any>) {
-        if (hasPerformance) performance.measure(task.name, startMarkName(task), endMarkName(task))
+        if (timingEnabled) performance.measure(`✳️ ${task.name}`, startMarkName(task), endMarkName(task))
     }
 }
\ No newline at end of file
diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts
index ba17219d3f3ed1d39ed855a0e5188ef2ba811311..cab7d59fcd15c3839d33722eed8d6d929908f7ec 100644
--- a/src/mol-theme/color.ts
+++ b/src/mol-theme/color.ts
@@ -26,11 +26,10 @@ import { ShapeGroupColorThemeProvider } from './color/shape-group';
 import { UnitIndexColorThemeProvider } from './color/unit-index';
 import { ScaleLegend } from 'mol-util/color/scale';
 import { TableLegend } from 'mol-util/color/tables';
+import { UncertaintyColorThemeProvider } from './color/uncertainty';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
-export type ColorThemeProps = { [k: string]: any }
-
 export { ColorTheme }
 interface ColorTheme<P extends PD.Params> {
     readonly factory: ColorTheme.Factory<P>
@@ -58,6 +57,8 @@ namespace ColorTheme {
     export function createRegistry() {
         return new ThemeRegistry(BuiltInColorThemes as { [k: string]: Provider<any> }, EmptyProvider)
     }
+
+    export type ParamValues<C extends ColorTheme.Provider<any>> = C extends ColorTheme.Provider<infer P> ? PD.Values<P> : never
 }
 
 export const BuiltInColorThemes = {
@@ -73,6 +74,8 @@ export const BuiltInColorThemes = {
     'secondary-structure': SecondaryStructureColorThemeProvider,
     'sequence-id': SequenceIdColorThemeProvider,
     'shape-group': ShapeGroupColorThemeProvider,
+    'uncertainty': UncertaintyColorThemeProvider,
     'unit-index': UnitIndexColorThemeProvider,
     'uniform': UniformColorThemeProvider,
-}
\ No newline at end of file
+}
+export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes
\ No newline at end of file
diff --git a/src/mol-theme/color/uncertainty.ts b/src/mol-theme/color/uncertainty.ts
new file mode 100644
index 0000000000000000000000000000000000000000..874f441a3c59a9b30f3996fe58ffbcfcbc1a61dd
--- /dev/null
+++ b/src/mol-theme/color/uncertainty.ts
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color, ColorScale } from 'mol-util/color';
+import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorTheme } from '../color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from '../theme';
+import { ColorListName, ColorListOptions } from 'mol-util/color/scale';
+
+const DefaultUncertaintyColor = Color(0xffff99)
+const Description = `Assigns a color based on the uncertainty of an element's position, , e.g. B-factor or RMSF, depending on the data availability and experimental technique.`
+
+export const UncertaintyColorThemeParams = {
+    domain: PD.Interval([0, 100]),
+    list: PD.ColorScale<ColorListName>('RedWhiteBlue', ColorListOptions),
+}
+export type UncertaintyColorThemeParams = typeof UncertaintyColorThemeParams
+export function getUncertaintyColorThemeParams(ctx: ThemeDataContext) {
+    return UncertaintyColorThemeParams // TODO return copy
+}
+
+export function getUncertainty(unit: Unit, element: ElementIndex): number {
+    if (Unit.isAtomic(unit)) {
+        return unit.model.atomicConformation.B_iso_or_equiv.value(element)
+    } else if (Unit.isSpheres(unit)) {
+        return unit.model.coarseConformation.spheres.rmsf[element]
+    } else {
+        return 0
+    }
+}
+
+export function UncertaintyColorTheme(ctx: ThemeDataContext, props: PD.Values<UncertaintyColorThemeParams>): ColorTheme<UncertaintyColorThemeParams> {
+    const scale = ColorScale.create({
+        reverse: true,
+        domain: props.domain,
+        listOrName: props.list,
+    })
+
+    // TODO calc domain based on data, set min/max as 10/90 percentile to be robust against outliers
+
+    function color(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            return scale.color(getUncertainty(location.unit, location.element))
+        } else if (Link.isLocation(location)) {
+            return scale.color(getUncertainty(location.aUnit, location.aUnit.elements[location.aIndex]))
+        }
+        return DefaultUncertaintyColor
+    }
+
+    return {
+        factory: UncertaintyColorTheme,
+        granularity: 'group',
+        color,
+        props,
+        description: Description,
+        legend: scale ? scale.legend : undefined
+    }
+}
+
+export const UncertaintyColorThemeProvider: ColorTheme.Provider<UncertaintyColorThemeParams> = {
+    label: 'Uncertainty',
+    factory: UncertaintyColorTheme,
+    getParams: getUncertaintyColorThemeParams,
+    defaultValues: PD.getDefaultValues(UncertaintyColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+}
\ No newline at end of file
diff --git a/src/mol-theme/overpaint.ts b/src/mol-theme/overpaint.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b1468590f574faca768519259cc7b003a6148d93
--- /dev/null
+++ b/src/mol-theme/overpaint.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Loci } from 'mol-model/loci';
+import { Color } from 'mol-util/color';
+
+export { Overpaint }
+
+type Overpaint = { layers: ReadonlyArray<Overpaint.Layer>, readonly alpha: number }
+
+namespace Overpaint {
+    export type Layer = { readonly loci: Loci, readonly color: Color }
+    export const Empty: Overpaint = { layers: [], alpha: 1 }
+
+    export function areEqual(oA: Overpaint, oB: Overpaint) {
+        if (oA.layers.length === 0 && oB.layers.length === 0) return true
+        if (oA.layers.length !== oB.layers.length) return false
+        if (oA.alpha !== oB.alpha) return false
+        for (let i = 0, il = oA.layers.length; i < il; ++i) {
+            if (oA.layers[i].color !== oB.layers[i].color) return false
+            if (!Loci.areEqual(oA.layers[i].loci, oB.layers[i].loci)) return false
+        }
+        return true
+    }
+}
\ No newline at end of file
diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts
index 52469b7a1dff63f6b1b740e50e6abd96347827fb..2bed1e52e9307d1ee5d82e0a1db109b485bbdf4b 100644
--- a/src/mol-theme/size.ts
+++ b/src/mol-theme/size.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>
  */
@@ -11,6 +11,7 @@ import { ThemeDataContext, ThemeRegistry, ThemeProvider } from 'mol-theme/theme'
 import { PhysicalSizeThemeProvider } from './size/physical';
 import { deepEqual } from 'mol-util';
 import { ShapeGroupSizeThemeProvider } from './size/shape-group';
+import { UncertaintySizeThemeProvider } from './size/uncertainty';
 
 export { SizeTheme }
 interface SizeTheme<P extends PD.Params> {
@@ -37,10 +38,14 @@ namespace SizeTheme {
     export function createRegistry() {
         return new ThemeRegistry(BuiltInSizeThemes as { [k: string]: Provider<any> }, EmptyProvider)
     }
+
+    export type ParamValues<C extends SizeTheme.Provider<any>> = C extends SizeTheme.Provider<infer P> ? PD.Values<P> : never
 }
 
 export const BuiltInSizeThemes = {
     'physical': PhysicalSizeThemeProvider,
     'shape-group': ShapeGroupSizeThemeProvider,
+    'uncertainty': UncertaintySizeThemeProvider,
     'uniform': UniformSizeThemeProvider
-}
\ No newline at end of file
+}
+export type BuiltInSizeThemeName = keyof typeof BuiltInSizeThemes
\ No newline at end of file
diff --git a/src/mol-theme/size/uncertainty.ts b/src/mol-theme/size/uncertainty.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d75a7f4ac7974a0273f98b3604669cb50411d36a
--- /dev/null
+++ b/src/mol-theme/size/uncertainty.ts
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement, Unit, Link, ElementIndex } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { SizeTheme } from '../size';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { ThemeDataContext } from 'mol-theme/theme';
+
+const Description = `Assigns a size reflecting the uncertainty of an element's position, e.g. B-factor or RMSF, depending on the data availability and experimental technique.`
+
+export const UncertaintySizeThemeParams = {
+    bfactorFactor: PD.Numeric(0.1, { min: 0, max: 1, step: 0.01 }),
+    rmsfFactor: PD.Numeric(0.05, { min: 0, max: 1, step: 0.01 }),
+    baseSize: PD.Numeric(0.2, { min: 0, max: 2, step: 0.1 }),
+}
+export type UncertaintySizeThemeParams = typeof UncertaintySizeThemeParams
+export function getUncertaintySizeThemeParams(ctx: ThemeDataContext) {
+    return UncertaintySizeThemeParams // TODO return copy
+}
+
+export function getUncertainty(unit: Unit, element: ElementIndex, props: PD.Values<UncertaintySizeThemeParams>): number {
+    if (Unit.isAtomic(unit)) {
+        return unit.model.atomicConformation.B_iso_or_equiv.value(element) * props.bfactorFactor
+    } else if (Unit.isSpheres(unit)) {
+        return unit.model.coarseConformation.spheres.rmsf[element] * props.rmsfFactor
+    } else {
+        return 0
+    }
+}
+
+export function UncertaintySizeTheme(ctx: ThemeDataContext, props: PD.Values<UncertaintySizeThemeParams>): SizeTheme<UncertaintySizeThemeParams> {
+    function size(location: Location): number {
+        let size = props.baseSize
+        if (StructureElement.isLocation(location)) {
+            size += getUncertainty(location.unit, location.element, props)
+        } else if (Link.isLocation(location)) {
+            size += getUncertainty(location.aUnit, location.aUnit.elements[location.aIndex], props)
+        }
+        return size
+    }
+
+    return {
+        factory: UncertaintySizeTheme,
+        granularity: 'group',
+        size,
+        props,
+        description: Description
+    }
+}
+
+export const UncertaintySizeThemeProvider: SizeTheme.Provider<UncertaintySizeThemeParams> = {
+    label: 'Uncertainty',
+    factory: UncertaintySizeTheme,
+    getParams: getUncertaintySizeThemeParams,
+    defaultValues: PD.getDefaultValues(UncertaintySizeThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure // TODO check if the structure has Uncertainty (B-factor, RMSF, ...) values
+}
\ No newline at end of file
diff --git a/src/mol-theme/theme.ts b/src/mol-theme/theme.ts
index b5d224268575463da7436c48865ddead95b9b27d..bdf11bfed8a356b97705d896a69371f4d403ef0c 100644
--- a/src/mol-theme/theme.ts
+++ b/src/mol-theme/theme.ts
@@ -64,6 +64,7 @@ function getTypes(list: { name: string, provider: ThemeProvider<any, any> }[]) {
 export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
     private _list: { name: string, provider: ThemeProvider<T, any> }[] = []
     private _map = new Map<string, ThemeProvider<T, any>>()
+    private _name = new Map<ThemeProvider<T, any>, string>()
 
     get default() { return this._list[0]; }
     get list() { return this._list }
@@ -76,18 +77,28 @@ export class ThemeRegistry<T extends ColorTheme<any> | SizeTheme<any>> {
     add<P extends PD.Params>(name: string, provider: ThemeProvider<T, P>) {
         this._list.push({ name, provider })
         this._map.set(name, provider)
+        this._name.set(provider, name)
     }
 
     remove(name: string) {
         this._list.splice(this._list.findIndex(e => e.name === name), 1)
-        this._map.delete(name)
-        console.log('removed', name, this._list, this._map)
+        const p = this._map.get(name);
+        if (p) {
+            this._map.delete(name);
+            this._name.delete(p);
+        }
     }
 
     get<P extends PD.Params>(name: string): ThemeProvider<T, P> {
         return this._map.get(name) || this.emptyProvider
     }
 
+    getName(provider: ThemeProvider<T, any>): string {
+        if (!this._name.has(provider)) throw new Error(`'${provider.label}' is not a registered represenatation provider.`);
+        return this._name.get(provider)!;
+    }
+
+
     create(name: string, ctx: ThemeDataContext, props = {}) {
         const provider = this.get(name)
         return provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props })
diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts
index 95b5ef5be19ea24383680e0e6ca84054cf6c70d7..078ab5319b4db6bdd612c7655390a8e4a0e2225f 100644
--- a/src/mol-util/array.ts
+++ b/src/mol-util/array.ts
@@ -56,4 +56,21 @@ export function arrayRms(array: NumberArray) {
 export function fillSerial<T extends NumberArray> (array: T, n?: number) {
     for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i
     return array
+}
+
+export function arrayRemoveInPlace<T>(xs: T[], x: T) {
+    let i = 0, l = xs.length, found = false;
+    for (; i < l; i++) {
+        if (xs[i] === x) {
+            found = true;
+            break;
+        }
+    }
+    if (!found) return false;
+    i++;
+    for (; i < l; i++) {
+        xs[i - 1] = xs[i];
+    }
+    xs.pop();
+    return true;
 }
\ No newline at end of file
diff --git a/src/mol-util/async-queue.ts b/src/mol-util/async-queue.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5e01ff69f4e434b6821330fa134040a03520ae2f
--- /dev/null
+++ b/src/mol-util/async-queue.ts
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { arrayRemoveInPlace } from './array';
+import { Subject } from 'rxjs';
+
+export class AsyncQueue<T> {
+    private queue: T[] = [];
+    private signal = new Subject<{ v: T, stillPresent: boolean }>();
+
+    enqueue(v: T) {
+        this.queue.push(v);
+        if (this.queue.length === 1) return true;
+        return this.waitFor(v);
+    }
+
+    handled(v: T) {
+        arrayRemoveInPlace(this.queue, v);
+        if (this.queue.length > 0) {
+            this.signal.next({ v: this.queue[0], stillPresent: true });
+        }
+    }
+
+    remove(v: T) {
+        const rem = arrayRemoveInPlace(this.queue, v);
+        if (rem)
+        this.signal.next({ v, stillPresent: false })
+        return rem;
+    }
+
+    private waitFor(t: T): Promise<boolean> {
+        return new Promise(res => {
+            const sub = this.signal.subscribe(({ v, stillPresent: removed }) => {
+                if (v === t) {
+                    sub.unsubscribe();
+                    res(removed);
+                }
+            });
+        })
+    }
+}
\ No newline at end of file
diff --git a/src/mol-util/bit-flags.ts b/src/mol-util/bit-flags.ts
index 7f0bb8870771381b93be9e199f14e8bd52a4394e..2a76338f92d25dc047fee6abcfe769c07b5756f5 100644
--- a/src/mol-util/bit-flags.ts
+++ b/src/mol-util/bit-flags.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-interface BitFlags<Flags> extends Number { '@type': Flags }
+type BitFlags<Flags> = number & Flags
 
 namespace BitFlags {
     export function create<F>(flags: F): BitFlags<F> { return flags as any; }
diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts
index 05730c20f0c7c8c296bfb35462928624b425a26a..f451719aeaae9f8f8db103a9a3ddbf98bd093961 100644
--- a/src/mol-util/data-source.ts
+++ b/src/mol-util/data-source.ts
@@ -9,16 +9,16 @@
 import { Task, RuntimeContext } from 'mol-task';
 import { utf8Read } from 'mol-io/common/utf8';
 
-export enum DataCompressionMethod {
-    None,
-    Gzip
-}
+// export enum DataCompressionMethod {
+//     None,
+//     Gzip
+// }
 
-export interface AjaxGetParams {
+export interface AjaxGetParams<T extends 'string' | 'binary' | 'json' = 'string'> {
     url: string,
-    type: 'string' | 'binary',
+    type?: T,
     title?: string,
-    compression?: DataCompressionMethod
+    // compression?: DataCompressionMethod
     body?: string
 }
 
@@ -34,19 +34,18 @@ export function readFromFile(file: File, type: 'string' | 'binary') {
     return <Task<Uint8Array | string>>readFromFileInternal(file, type === 'binary');
 }
 
-export function ajaxGetString(url: string, title?: string) {
-    return <Task<string>>ajaxGetInternal(title, url, false, false);
-}
-
-export function ajaxGetUint8Array(url: string, title?: string) {
-    return <Task<Uint8Array>>ajaxGetInternal(title, url, true, false);
+// TODO: support for no-referrer
+export function ajaxGet(url: string): Task<string>
+export function ajaxGet(params: AjaxGetParams<'string'>): Task<string>
+export function ajaxGet(params: AjaxGetParams<'binary'>): Task<Uint8Array>
+export function ajaxGet<T = any>(params: AjaxGetParams<'json'>): Task<T>
+export function ajaxGet(params: AjaxGetParams<'string' | 'binary' | 'json'>): Task<string | Uint8Array | object>
+export function ajaxGet(params: AjaxGetParams<'string' | 'binary' | 'json'> | string) {
+    if (typeof params === 'string') return ajaxGetInternal(params, params, 'string', false);
+    return ajaxGetInternal(params.title, params.url, params.type || 'string', false /* params.compression === DataCompressionMethod.Gzip */, params.body);
 }
 
-export function ajaxGet(params: AjaxGetParams) {
-    return <Task<string | Uint8Array>>ajaxGetInternal(params.title, params.url, params.type === 'binary', params.compression === DataCompressionMethod.Gzip, params.body);
-}
-
-export type AjaxTask = (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>
+export type AjaxTask = typeof ajaxGet
 
 function decompress(buffer: Uint8Array): Uint8Array {
     // TODO
@@ -163,10 +162,11 @@ async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompres
     }
 }
 
-function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: boolean, decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
+function ajaxGetInternal(title: string | undefined, url: string, type: 'json' | 'string' | 'binary', decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
     let xhttp: XMLHttpRequest | undefined = void 0;
     return Task.create(title ? title : 'Download', async ctx => {
         try {
+            const asUint8Array = type === 'binary';
             if (!asUint8Array && decompressGzip) {
                 throw 'Decompress is only available when downloading binary data.';
             }
@@ -180,6 +180,13 @@ function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: b
             ctx.update({ message: 'Waiting for server...', canAbort: true });
             const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array);
             const result = await processAjax(ctx, asUint8Array, decompressGzip, e)
+
+            if (type === 'json') {
+                ctx.update({ message: 'Parsing JSON...', canAbort: false });
+                const data = JSON.parse(result);
+                return data;
+            }
+
             return result;
         } finally {
             xhttp = void 0;
@@ -187,4 +194,58 @@ function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: b
     }, () => {
         if (xhttp) xhttp.abort();
     });
+}
+
+export type AjaxGetManyEntry<T> = { kind: 'ok', id: string, result: T } | { kind: 'error', id: string, error: any }
+export async function ajaxGetMany(ctx: RuntimeContext, sources: { id: string, url: string, isBinary?: boolean, canFail?: boolean }[], maxConcurrency: number) {
+    const len = sources.length;
+    const slots: AjaxGetManyEntry<string | Uint8Array>[] = new Array(sources.length);
+
+    await ctx.update({ message: 'Downloading...', current: 0, max: len });
+    let promises: Promise<AjaxGetManyEntry<any> & { index: number }>[] = [], promiseKeys: number[] = [];
+    let currentSrc = 0;
+    for (let _i = Math.min(len, maxConcurrency); currentSrc < _i; currentSrc++) {
+        const current = sources[currentSrc];
+        promises.push(wrapPromise(currentSrc, current.id, ajaxGet({ url: current.url, type: current.isBinary ? 'binary' : 'string' }).runAsChild(ctx)));
+        promiseKeys.push(currentSrc);
+    }
+
+    let done = 0;
+    while (promises.length > 0) {
+        const r = await Promise.race(promises);
+        const src = sources[r.index];
+        const idx = promiseKeys.indexOf(r.index);
+        done++;
+        if (r.kind === 'error' && !src.canFail) {
+            // TODO: cancel other downloads
+            throw new Error(`${src.url}: ${r.error}`);
+        }
+        if (ctx.shouldUpdate) {
+            await ctx.update({ message: 'Downloading...', current: done, max: len });
+        }
+        slots[r.index] = r;
+        promises = promises.filter(_filterRemoveIndex, idx);
+        promiseKeys = promiseKeys.filter(_filterRemoveIndex, idx);
+        if (currentSrc < len) {
+            const current = sources[currentSrc];
+            promises.push(wrapPromise(currentSrc, current.id, ajaxGet({ url: current.url, type: current.isBinary ? 'binary' : 'string' }).runAsChild(ctx)));
+            promiseKeys.push(currentSrc);
+            currentSrc++;
+        }
+    }
+
+    return slots;
+}
+
+function _filterRemoveIndex(this: number, _: any, i: number) {
+    return this !== i;
+}
+
+async function wrapPromise<T>(index: number, id: string, p: Promise<T>): Promise<AjaxGetManyEntry<T> & { index: number }> {
+    try {
+        const result = await p;
+        return { kind: 'ok', result, index, id };
+    } catch (error) {
+        return { kind: 'error', error, index, id }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-util/date.ts b/src/mol-util/date.ts
index 90d03997b579862440fc0dc3a30c8663a6ee48cc..cfb4cdb3789979b23b4a26083e93386c97ca2b13 100644
--- a/src/mol-util/date.ts
+++ b/src/mol-util/date.ts
@@ -6,4 +6,15 @@
 
 export function dateToUtcString(date: Date) {
     return date.toISOString().replace(/T/, ' ').replace(/\..+/, '');
+}
+
+export function getFormattedTime() {
+    const today = new Date();
+    const y = today.getFullYear();
+    const m = today.getMonth() + 1;
+    const d = today.getDate();
+    const h = today.getHours();
+    const mi = today.getMinutes();
+    const s = today.getSeconds();
+    return y + '-' + m + '-' + d + '-' + h + '-' + mi + '-' + s;
 }
\ No newline at end of file
diff --git a/src/mol-util/graphql-client.ts b/src/mol-util/graphql-client.ts
index d6f8e5c719766380f22675f260e5f910425a12f9..174acc73278eb3617c07adeec62e27d6447e9e4a 100644
--- a/src/mol-util/graphql-client.ts
+++ b/src/mol-util/graphql-client.ts
@@ -6,7 +6,7 @@
  * Adapted from https://github.com/prisma/graphql-request, Copyright (c) 2017 Graphcool, MIT
  */
 
-import { Task, RuntimeContext } from 'mol-task';
+import { RuntimeContext } from 'mol-task';
 
 type Variables = { [key: string]: any }
 
@@ -58,7 +58,7 @@ export class ClientError extends Error {
 }
 
 export class GraphQLClient {
-    constructor(private url: string, private fetch: (url: string, type: 'string' | 'binary', body?: string) => Task<string | Uint8Array>) {
+    constructor(private url: string, private fetch: import('mol-util/data-source').AjaxTask) {
         this.url = url
     }
 
@@ -69,7 +69,7 @@ export class GraphQLClient {
             variables: variables ? variables : undefined,
         })
 
-        const resultStr = await this.fetch(this.url, 'string', body).runInContext(ctx) as string
+        const resultStr = await this.fetch({ url: this.url, body }).runInContext(ctx)
         const result = JSON.parse(resultStr)
 
         if (!result.errors && result.data) {
diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts
index 9c1aa24bfe624cbcd813ffdbf42bcbbcf3cfc6ef..73d5c415b5d46f3f69de80f8e9eec0a05651f8be 100644
--- a/src/mol-util/index.ts
+++ b/src/mol-util/index.ts
@@ -14,6 +14,8 @@ import { Progress } from 'mol-task';
 export * from './value-cell'
 export { BitFlags, StringBuilder, UUID, Mask }
 
+export const noop = function () { };
+
 export function round(n: number, d: number) {
     let f = Math.pow(10, d)
     return Math.round(f * n) / f
diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts
index 84ead2d65d6638c103ef0932ede0a4a3f4d924c6..8b08cb2fbbea2085a75b45c590c2ba38744e3658 100644
--- a/src/mol-util/input/input-observer.ts
+++ b/src/mol-util/input/input-observer.ts
@@ -1,10 +1,10 @@
 /**
- * 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 { Subject } from 'rxjs';
+import { Subject, Observable } from 'rxjs';
 
 import { Vec2 } from 'mol-math/linear-algebra';
 
@@ -39,7 +39,8 @@ function getButtons(event: MouseEvent | Touch) {
 
 export const DefaultInputObserverProps = {
     noScroll: true,
-    noContextMenu: true
+    noContextMenu: true,
+    noPinchZoom: true
 }
 export type InputObserverProps = Partial<typeof DefaultInputObserverProps>
 
@@ -49,11 +50,18 @@ export type ModifiersKeys = {
     control: boolean,
     meta: boolean
 }
+export namespace ModifiersKeys {
+    export const None: ModifiersKeys = { shift: false, alt: false, control: false, meta: false };
 
-export interface ButtonsType extends BitFlags<ButtonsType.Flag> { }
+    export function areEqual(a: ModifiersKeys, b: ModifiersKeys) {
+        return a.shift === b.shift && a.alt === b.alt && a.control === b.control && a.meta === b.meta;
+    }
+}
+
+export type ButtonsType = BitFlags<ButtonsType.Flag>
 
 export namespace ButtonsType {
-    export const has: (ss: ButtonsType, f: Flag) => boolean = BitFlags.has
+    export const has: (btn: ButtonsType, f: Flag) => boolean = BitFlags.has
     export const create: (fs: Flag) => ButtonsType = BitFlags.create
 
     export const enum Flag {
@@ -73,7 +81,7 @@ export namespace ButtonsType {
 }
 
 type BaseInput = {
-    buttons: number
+    buttons: ButtonsType
     modifiers: ModifiersKeys
 }
 
@@ -110,6 +118,7 @@ export type MoveInput = {
 
 export type PinchInput = {
     delta: number,
+    fraction: number,
     distance: number,
     isStart: boolean
 }
@@ -135,21 +144,24 @@ interface InputObserver {
     noScroll: boolean
     noContextMenu: boolean
 
-    drag: Subject<DragInput>,
-    wheel: Subject<WheelInput>,
-    pinch: Subject<PinchInput>,
-    click: Subject<ClickInput>,
-    move: Subject<MoveInput>,
-    leave: Subject<undefined>,
-    enter: Subject<undefined>,
-    resize: Subject<ResizeInput>,
+    drag: Observable<DragInput>,
+    // Equivalent to mouseUp and touchEnd
+    interactionEnd: Observable<undefined>,
+    wheel: Observable<WheelInput>,
+    pinch: Observable<PinchInput>,
+    click: Observable<ClickInput>,
+    move: Observable<MoveInput>,
+    leave: Observable<undefined>,
+    enter: Observable<undefined>,
+    resize: Observable<ResizeInput>,
+    modifiers: Observable<ModifiersKeys>
 
     dispose: () => void
 }
 
 namespace InputObserver {
     export function create (element: Element, props: InputObserverProps = {}): InputObserver {
-        let { noScroll, noContextMenu } = { ...DefaultInputObserverProps, ...props }
+        let { noScroll, noContextMenu, noPinchZoom } = { ...DefaultInputObserverProps, ...props }
 
         let lastTouchDistance = 0
         const pointerDown = Vec2.zero()
@@ -164,11 +176,17 @@ namespace InputObserver {
             meta: false
         }
 
+        function getModifiers(): ModifiersKeys {
+            return { ...modifiers };
+        }
+
         let dragging: DraggingState = DraggingState.Stopped
         let disposed = false
-        let buttons = 0
+        let buttons = 0 as ButtonsType
+        let isInside = false
 
         const drag = new Subject<DragInput>()
+        const interactionEnd = new Subject<undefined>();
         const click = new Subject<ClickInput>()
         const move = new Subject<MoveInput>()
         const wheel = new Subject<WheelInput>()
@@ -176,6 +194,7 @@ namespace InputObserver {
         const resize = new Subject<ResizeInput>()
         const leave = new Subject<undefined>()
         const enter = new Subject<undefined>()
+        const modifiersEvent = new Subject<ModifiersKeys>()
 
         attach()
 
@@ -186,6 +205,7 @@ namespace InputObserver {
             set noContextMenu (value: boolean) { noContextMenu = value },
 
             drag,
+            interactionEnd,
             wheel,
             pinch,
             click,
@@ -193,6 +213,7 @@ namespace InputObserver {
             leave,
             enter,
             resize,
+            modifiers: modifiersEvent,
 
             dispose
         }
@@ -215,9 +236,8 @@ namespace InputObserver {
             element.addEventListener('touchend', onTouchEnd as any, false)
 
             element.addEventListener('blur', handleBlur)
-            element.addEventListener('keyup', handleMods as EventListener)
-            element.addEventListener('keydown', handleMods as EventListener)
-            element.addEventListener('keypress', handleMods as EventListener)
+            window.addEventListener('keyup', handleKeyUp as EventListener, false)
+            window.addEventListener('keydown', handleKeyDown as EventListener, false)
 
             window.addEventListener('resize', onResize, false)
         }
@@ -241,9 +261,8 @@ namespace InputObserver {
             element.removeEventListener('touchend', onTouchEnd as any, false)
 
             element.removeEventListener('blur', handleBlur)
-            element.removeEventListener('keyup', handleMods as EventListener)
-            element.removeEventListener('keydown', handleMods as EventListener)
-            element.removeEventListener('keypress', handleMods as EventListener)
+            window.removeEventListener('keyup', handleKeyUp as EventListener, false)
+            window.removeEventListener('keydown', handleKeyDown as EventListener, false)
 
             window.removeEventListener('resize', onResize, false)
         }
@@ -256,16 +275,30 @@ namespace InputObserver {
 
         function handleBlur () {
             if (buttons || modifiers.shift || modifiers.alt || modifiers.meta || modifiers.control) {
-                buttons = 0
+                buttons = 0 as ButtonsType
                 modifiers.shift = modifiers.alt = modifiers.control = modifiers.meta = false
             }
         }
 
-        function handleMods (event: MouseEvent | KeyboardEvent) {
-            if ('altKey' in event) modifiers.alt = !!event.altKey
-            if ('shiftKey' in event) modifiers.shift = !!event.shiftKey
-            if ('ctrlKey' in event) modifiers.control = !!event.ctrlKey
-            if ('metaKey' in event) modifiers.meta = !!event.metaKey
+        function handleKeyDown (event: KeyboardEvent) {
+            let changed = false;
+            if (!modifiers.alt && event.altKey) { changed = true; modifiers.alt = true; }
+            if (!modifiers.shift && event.shiftKey) { changed = true; modifiers.shift = true; }
+            if (!modifiers.control && event.ctrlKey) { changed = true; modifiers.control = true; }
+            if (!modifiers.meta && event.metaKey) { changed = true; modifiers.meta = true; }
+
+            if (changed && isInside) modifiersEvent.next(getModifiers());
+        }
+
+        function handleKeyUp (event: KeyboardEvent) {
+            let changed = false;
+
+            if (modifiers.alt && !event.altKey) { changed = true; modifiers.alt = false; }
+            if (modifiers.shift && !event.shiftKey) { changed = true; modifiers.shift = false; }
+            if (modifiers.control && !event.ctrlKey) { changed = true; modifiers.control = false; }
+            if (modifiers.meta && !event.metaKey) { changed = true; modifiers.meta = false; }
+
+            if (changed && isInside) modifiersEvent.next(getModifiers());
         }
 
         function getCenterTouch (ev: TouchEvent): PointerEvent {
@@ -293,24 +326,39 @@ namespace InputObserver {
                 buttons = ButtonsType.Flag.Secondary
                 onPointerDown(getCenterTouch(ev))
 
-                pinch.next({ distance: lastTouchDistance, delta: 0, isStart: true })
+                const touchDistance = getTouchDistance(ev)
+                lastTouchDistance = touchDistance
+                pinch.next({ distance: touchDistance, fraction: 1, delta: 0, isStart: true })
             }
         }
 
-        function onTouchEnd (ev: TouchEvent) {}
+        function onTouchEnd (ev: TouchEvent) {
+            endDrag()
+        }
 
         function onTouchMove (ev: TouchEvent) {
+            if (noPinchZoom) {
+                ev.preventDefault();
+                ev.stopPropagation();
+                if ((ev as any).originalEvent) {
+                    (ev as any).originalEvent.preventDefault();
+                    (ev as any).originalEvent.stopPropagation();
+                }
+            }
+
             if (ev.touches.length === 1) {
                 buttons = ButtonsType.Flag.Primary
                 onPointerMove(ev.touches[0])
             } else if (ev.touches.length >= 2) {
                 const touchDistance = getTouchDistance(ev)
-                if (lastTouchDistance - touchDistance < 4) {
+                const touchDelta = lastTouchDistance - touchDistance
+                if (Math.abs(touchDelta) < 4) {
                     buttons = ButtonsType.Flag.Secondary
                     onPointerMove(getCenterTouch(ev))
                 } else {
                     pinch.next({
-                        delta: lastTouchDistance - touchDistance,
+                        delta: touchDelta,
+                        fraction: lastTouchDistance / touchDistance,
                         distance: touchDistance,
                         isStart: false
                     })
@@ -331,6 +379,11 @@ namespace InputObserver {
 
         function onMouseUp (ev: MouseEvent) {
             onPointerUp(ev)
+            endDrag()
+        }
+
+        function endDrag() {
+            interactionEnd.next()
         }
 
         function onPointerDown (ev: PointerEvent) {
@@ -350,7 +403,7 @@ namespace InputObserver {
                 const { pageX, pageY } = ev
                 const [ x, y ] = pointerEnd
 
-                click.next({ x, y, pageX, pageY, buttons, modifiers })
+                click.next({ x, y, pageX, pageY, buttons, modifiers: getModifiers() })
             }
         }
 
@@ -359,7 +412,7 @@ namespace InputObserver {
             const { pageX, pageY } = ev
             const [ x, y ] = pointerEnd
             const inside = insideBounds(pointerEnd)
-            move.next({ x, y, pageX, pageY, buttons, modifiers, inside })
+            move.next({ x, y, pageX, pageY, buttons, modifiers: getModifiers(), inside })
 
             if (dragging === DraggingState.Stopped) return
 
@@ -367,7 +420,7 @@ namespace InputObserver {
 
             const isStart = dragging === DraggingState.Started
             const [ dx, dy ] = pointerDelta
-            drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers, isStart })
+            drag.next({ x, y, dx, dy, pageX, pageY, buttons, modifiers: getModifiers(), isStart })
 
             Vec2.copy(pointerStart, pointerEnd)
             dragging = DraggingState.Moving
@@ -390,15 +443,17 @@ namespace InputObserver {
             const dz = (ev.deltaZ || 0) * scale
 
             if (dx || dy || dz) {
-                wheel.next({ dx, dy, dz, buttons, modifiers })
+                wheel.next({ dx, dy, dz, buttons, modifiers: getModifiers() })
             }
         }
 
         function onMouseEnter (ev: Event) {
+            isInside = true;
             enter.next();
         }
 
         function onMouseLeave (ev: Event) {
+            isInside = false;
             leave.next();
         }
 
diff --git a/src/mol-util/lru-cache.ts b/src/mol-util/lru-cache.ts
new file mode 100644
index 0000000000000000000000000000000000000000..38d950981e8cf5d7b5bfebda70c8ac0c52d48445
--- /dev/null
+++ b/src/mol-util/lru-cache.ts
@@ -0,0 +1,53 @@
+
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol.
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { LinkedList } from 'mol-data/generic';
+
+export { LRUCache }
+
+interface LRUCache<T> {
+    entries: LinkedList<LRUCache.Entry<T>>,
+    capacity: number
+}
+
+namespace LRUCache {
+    export interface Entry<T> {
+        key: string,
+        data: T
+    }
+
+    function entry<T>(key: string, data: T): Entry<T> {
+        return { key, data };
+    }
+
+    export function create<T>(capacity: number): LRUCache<T> {
+        return {
+            entries: LinkedList<Entry<T>>(),
+            capacity: Math.max(1, capacity)
+        };
+    }
+
+    export function get<T>(cache: LRUCache<T>, key: string) {
+        for (let e = cache.entries.first; e; e = e.next) {
+            if (e.value.key === key) {
+                cache.entries.remove(e);
+                cache.entries.addLast(e.value);
+                return e.value.data;
+            }
+        }
+        return void 0;
+    }
+
+    export function set<T>(cache: LRUCache<T>, key: string, data: T): T {
+        if (cache.entries.count >= cache.capacity) {
+            cache.entries.remove(cache.entries.first!);
+        }
+        cache.entries.addLast(entry(key, data));
+        return data;
+    }
+}
\ No newline at end of file
diff --git a/src/mol-util/memoize.ts b/src/mol-util/memoize.ts
index d74f25380edcf97df9cce41639e6aeb5f16944b4..c91d1f3e748b06b689782da9439093d92d774061 100644
--- a/src/mol-util/memoize.ts
+++ b/src/mol-util/memoize.ts
@@ -4,6 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+ /** Cache the latest result from calls to a function with any number of arguments */
 export function memoizeLatest<Args extends any[], T>(f: (...args: Args) => T): (...args: Args) => T {
     let lastArgs: any[] | undefined = void 0, value: any = void 0;
     return (...args) => {
@@ -23,6 +24,7 @@ export function memoizeLatest<Args extends any[], T>(f: (...args: Args) => T): (
     }
 }
 
+/** Cache all results from calls to a function with a single argument */
 export function memoize1<A, T>(f: (a: A) => T): (a: A) => T {
     const cache = new Map<A, T>();
     return a => {
diff --git a/src/mol-util/now.ts b/src/mol-util/now.ts
index c9c9f4f631b9b790340b326591fd0f0e8819d4fd..7a88af574b4b70147b5d99127c19d46abbd07ffb 100644
--- a/src/mol-util/now.ts
+++ b/src/mol-util/now.ts
@@ -11,7 +11,7 @@ const now: () => now.Timestamp = (function () {
     if (typeof window !== 'undefined' && window.performance) {
         const perf = window.performance;
         return () => perf.now();
-    } else if (typeof process !== 'undefined' && process.hrtime !== 'undefined') {
+    } else if (typeof process !== 'undefined' && process.hrtime !== 'undefined' && typeof process.hrtime === 'function') {
         return () => {
             const t = process.hrtime();
             return t[0] * 1000 + t[1] / 1000000;
@@ -28,7 +28,7 @@ namespace now {
 }
 
 
-function formatTimespan(t: number) {
+function formatTimespan(t: number, includeMsZeroes = true) {
     if (isNaN(t)) return 'n/a';
 
     let h = Math.floor(t / (60 * 60 * 1000)),
@@ -37,6 +37,7 @@ function formatTimespan(t: number) {
         ms = Math.floor(t % 1000).toString();
 
     while (ms.length < 3) ms = '0' + ms;
+    while (!includeMsZeroes && ms.length > 1 && ms[ms.length - 1] === '0') ms = ms.substr(0, ms.length - 1);
 
     if (h > 0) return `${h}h${m}m${s}.${ms}s`;
     if (m > 0) return `${m}m${s}.${ms}s`;
diff --git a/src/mol-util/object.ts b/src/mol-util/object.ts
index 8521c9675777557b1e537ab0e69445c1c815eb19..bfb4824f5202bec67115a979304f11ccc8f30716 100644
--- a/src/mol-util/object.ts
+++ b/src/mol-util/object.ts
@@ -41,6 +41,10 @@ export function shallowEqual<T>(a: T, b: T) {
 }
 
 export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T {
+    return shallowMergeArray(source, rest);
+}
+
+export function shallowMergeArray<T>(source: T, rest: (Partial<T> | undefined)[]): T {
     // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
     let ret: any = source;
 
@@ -78,4 +82,12 @@ export function deepClone<T>(source: T): T {
     }
 
     throw new Error(`Can't clone, type "${typeof source}" unsupported`);
+}
+
+export function mapObjectMap<O extends { [k: string]: T }, T, S>(o: O, f: (v: T) => S): { [k: string]: S } {
+    const ret: any = { };
+    for (const k of Object.keys(o)) {
+        ret[k] = f((o as any)[k]);
+    }
+    return ret;
 }
\ No newline at end of file
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index ec68757b0c2ce572a14884f91bc388c35629c194..a7f08f3cf1a3d8227703292d4c4078aeb862e499 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -14,13 +14,15 @@ export namespace ParamDefinition {
     export interface Info {
         label?: string,
         description?: string,
+        fieldLabels?: { [name: string]: string },
         isHidden?: boolean,
     }
 
     function setInfo<T extends Info>(param: T, info?: Info): T {
         if (!info) return param;
-        if (info.description) param.description = info.description;
         if (info.label) param.label = info.label;
+        if (info.description) param.description = info.description;
+        if (info.fieldLabels) param.fieldLabels = info.fieldLabels;
         if (info.isHidden) param.isHidden = info.isHidden;
         return param;
     }
@@ -30,9 +32,14 @@ export namespace ParamDefinition {
         defaultValue: T
     }
 
-    export function makeOptional<T>(p: Base<T>): Base<T | undefined> {
-        p.isOptional = true;
-        return p;
+    export interface Optional<T extends Any = Any> extends Base<T['defaultValue'] | undefined> {
+        type: T['type']
+    }
+
+    export function Optional<T>(p: Base<T>): Base<T | undefined> {
+        const ret = { ...p };
+        ret.isOptional = true;
+        return ret;
     }
 
     export interface Value<T> extends Base<T> {
@@ -153,8 +160,8 @@ export namespace ParamDefinition {
         isExpanded?: boolean,
         isFlat?: boolean
     }
-    export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean, isFlat?: boolean }): Group<Values<P>> {
-        const ret = setInfo<Group<Values<P>>>({ type: 'group', defaultValue: getDefaultValues(params) as any, params }, info);
+    export function Group<T>(params: For<T>, info?: Info & { isExpanded?: boolean, isFlat?: boolean }): Group<Normalize<T>> {
+        const ret = setInfo<Group<Normalize<T>>>({ type: 'group', defaultValue: getDefaultValues(params as any as Params) as any, params: params as any as Params }, info);
         if (info && info.isExpanded) ret.isExpanded = info.isExpanded;
         if (info && info.isFlat) ret.isFlat = info.isFlat;
         return ret;
@@ -187,10 +194,21 @@ export namespace ParamDefinition {
         }, info);
     }
 
+    export interface ObjectList<T = any> extends Base<T[]> {
+        type: 'object-list',
+        element: Params,
+        ctor(): T,
+        getLabel(t: T): string
+    }
+    export function ObjectList<T>(element: For<T>, getLabel: (e: T) => string, info?: Info & { defaultValue?: T[], ctor?: () => T }): ObjectList<Normalize<T>> {
+        return setInfo<ObjectList<Normalize<T>>>({ type: 'object-list', element: element as any as Params, getLabel, ctor: _defaultObjectListCtor, defaultValue: (info && info.defaultValue) || []  });
+    }
+    function _defaultObjectListCtor(this: ObjectList) { return getDefaultValues(this.element) as any; }
+
     export interface Converted<T, C> extends Base<T> {
         type: 'converted',
         converted: Any,
-        /** converts from props value to display value */
+        /** converts from prop value to display value */
         fromValue(v: T): C,
         /** converts from display value to prop value */
         toValue(v: C): T
@@ -199,7 +217,28 @@ export namespace ParamDefinition {
         return { type: 'converted', defaultValue: toValue(converted.defaultValue), converted, fromValue, toValue };
     }
 
-    export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph | ColorScale<any> | Group<any> | Mapped<any> | Converted<any, any>
+    export interface Conditioned<T, P extends Base<T>, C = { [k: string]: P }> extends Base<T> {
+        type: 'conditioned',
+        select: Select<string>,
+        conditionParams: C
+        conditionForValue(v: T): keyof C
+        conditionedValue(v: T, condition: keyof C): T,
+    }
+    export function Conditioned<T, P extends Base<T>, C = { [k: string]: P }>(defaultValue: T, conditionParams: C, conditionForValue: (v: T) => keyof C, conditionedValue: (v: T, condition: keyof C) => T): Conditioned<T, P, C> {
+        const options = Object.keys(conditionParams).map(k => [k, k]) as [string, string][];
+        return { type: 'conditioned', select: Select<string>(conditionForValue(defaultValue) as string, options), defaultValue, conditionParams, conditionForValue, conditionedValue };
+    }
+
+    export interface ScriptExpression extends Base<{ language: 'mol-script', expression: string }> {
+        type: 'script-expression'
+    }
+    export function ScriptExpression(defaultValue: ScriptExpression['defaultValue'], info?: Info): ScriptExpression {
+        return setInfo<ScriptExpression>({ type: 'script-expression', defaultValue }, info)
+    }
+
+    export type Any =
+        | Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph
+        | ColorScale<any> | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | ScriptExpression | ObjectList
 
     export type Params = { [k: string]: Any }
     export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] }
@@ -287,6 +326,17 @@ export namespace ParamDefinition {
             return true;
         } else if (p.type === 'vec3') {
             return Vec3Data.equals(a, b);
+        } else if (p.type === 'script-expression') {
+            const u = a as ScriptExpression['defaultValue'], v = b as ScriptExpression['defaultValue'];
+            return u.language === v.language && u.expression === v.expression;
+        } else if (p.type === 'object-list') {
+            const u = a as ObjectList['defaultValue'], v = b as ObjectList['defaultValue'];
+            const l = u.length;
+            if (l !== v.length) return false;
+            for (let i = 0; i < l; i++) {
+                if (!areEqual(p.element, u[i], v[i])) return false;
+            }
+            return true;
         } else if (typeof a === 'object' && typeof b === 'object') {
             return shallowEqual(a, b);
         }
diff --git a/src/mol-util/set.ts b/src/mol-util/set.ts
index 7bc7e10d46c2d4b3937b2c41669680a30cac7bed..3d5f648d9a58f12450353da24cd3f5cd9ade0dc7 100644
--- a/src/mol-util/set.ts
+++ b/src/mol-util/set.ts
@@ -6,52 +6,63 @@
 
 // TODO remove Array.from workaround when targeting ES6
 
-/** Test if set a contains all elements of set b. */
-export function isSuperset<T>(setA: Set<T>, setB: Set<T>) {
-    for (const elm of Array.from(setB)) {
-        if (!setA.has(elm)) return false;
+export namespace SetUtils {
+    /** Test if set a contains all elements of set b. */
+    export function isSuperset<T>(setA: Set<T>, setB: Set<T>) {
+        for (const elm of Array.from(setB)) {
+            if (!setA.has(elm)) return false;
+        }
+        return true;
     }
-    return true;
-}
-
-/** Create set containing elements of both set a and set b. */
-export function union<T>(setA: Set<T>, setB: Set<T>): Set<T> {
-    const union = new Set(setA);
-    for (const elem of Array.from(setB)) union.add(elem);
-    return union;
-}
-
-export function unionMany<T>(sets: Set<T>[]) {
-    if (sets.length === 0) return new Set<T>();
-    if (sets.length === 1) return sets[0];
-    const union = new Set(sets[0]);
-    for (let i = 1; i < sets.length; i++) {
-        for (const elem of Array.from(sets[i])) union.add(elem);
+
+    /** Create set containing elements of both set a and set b. */
+    export function union<T>(setA: Set<T>, setB: Set<T>): Set<T> {
+        const union = new Set(setA);
+        for (const elem of Array.from(setB)) union.add(elem);
+        return union;
     }
-    return union;
-}
-
-export function unionManyArrays<T>(arrays: T[][]) {
-    if (arrays.length === 0) return new Set<T>();
-    const union = new Set(arrays[0]);
-    for (let i = 1; i < arrays.length; i++) {
-        for (const elem of arrays[i]) union.add(elem);
+
+    export function unionMany<T>(...sets: Set<T>[]) {
+        if (sets.length === 0) return new Set<T>();
+        if (sets.length === 1) return sets[0];
+        const union = new Set(sets[0]);
+        for (let i = 1; i < sets.length; i++) {
+            for (const elem of Array.from(sets[i])) union.add(elem);
+        }
+        return union;
     }
-    return union;
-}
-
-/** Create set containing elements of set a that are also in set b. */
-export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
-    const intersection = new Set();
-    for (const elem of Array.from(setB)) {
-        if (setA.has(elem)) intersection.add(elem);
+
+    export function unionManyArrays<T>(arrays: T[][]) {
+        if (arrays.length === 0) return new Set<T>();
+        const union = new Set(arrays[0]);
+        for (let i = 1; i < arrays.length; i++) {
+            for (const elem of arrays[i]) union.add(elem);
+        }
+        return union;
+    }
+
+    /** Create set containing elements of set a that are also in set b. */
+    export function intersection<T>(setA: Set<T>, setB: Set<T>): Set<T> {
+        const intersection = new Set();
+        for (const elem of Array.from(setB)) {
+            if (setA.has(elem)) intersection.add(elem);
+        }
+        return intersection;
+    }
+
+    /** Create set containing elements of set a that are not in set b. */
+    export function difference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
+        const difference = new Set(setA);
+        for (const elem of Array.from(setB)) difference.delete(elem);
+        return difference;
+    }
+
+    /** Test if set a and b contain the same elements. */
+    export function areEqual<T>(setA: Set<T>, setB: Set<T>) {
+        if (setA.size !== setB.size) return false
+        for (const elm of Array.from(setB)) {
+            if (!setA.has(elm)) return false;
+        }
+        return true;
     }
-    return intersection;
-}
-
-/** Create set containing elements of set a that are not in set b. */
-export function difference<T>(setA: Set<T>, setB: Set<T>): Set<T> {
-    const difference = new Set(setA);
-    for (const elem of Array.from(setB)) difference.delete(elem);
-    return difference;
 }
\ No newline at end of file
diff --git a/src/mol-util/string.ts b/src/mol-util/string.ts
index f2e8f5958ba30fd56124d1844cb478f45e1caa14..62ebf10cf0c16383d93237f2e008d1d1fb48bf5a 100644
--- a/src/mol-util/string.ts
+++ b/src/mol-util/string.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>
  */
@@ -37,4 +37,19 @@ export function snakeCaseToWords(str: string) {
 
 export function stringToWords(str: string) {
     return capitalize(splitCamelCase(splitSnakeCase(str)))
+}
+
+export function substringStartsWith(str: string, start: number, end: number, target: string) {
+    let len = target.length;
+    if (len > end - start) return false;
+    for (let i = 0; i < len; i++) {
+        if (str.charCodeAt(start + i) !== target.charCodeAt(i)) return false;
+    }
+    return true;
+}
+
+export function interpolate(str: string, params: { [k: string]: any }) {
+    const names = Object.keys(params);
+    const values = Object.values(params);
+    return new Function(...names, `return \`${str}\`;`)(...values);
 }
\ No newline at end of file
diff --git a/src/mol-util/url-query.ts b/src/mol-util/url.ts
similarity index 72%
rename from src/mol-util/url-query.ts
rename to src/mol-util/url.ts
index a4a10a8371c24b55995ea175df35fb14bc6cec86..3234bdbaea7ff7565eb8f2542c057b314242fd02 100644
--- a/src/mol-util/url-query.ts
+++ b/src/mol-util/url.ts
@@ -9,4 +9,8 @@ export function urlQueryParameter (id: string) {
     const a = new RegExp(`${id}=([^&#=]*)`)
     const m = a.exec(window.location.search)
     return m ? decodeURIComponent(m[1]) : undefined
+}
+
+export function urlCombine(base: string, query: string) {
+    return `${base}${base[base.length - 1] === '/' || query[0] === '/' ? '' : '/'}${query}`;
 }
\ No newline at end of file
diff --git a/src/mol-util/uuid.ts b/src/mol-util/uuid.ts
index 5ddf5bb22cd9f6e4952fecf699609208d59ec740..c1ebe8808a7ef744f6d10777938d16c8bc12c65f 100644
--- a/src/mol-util/uuid.ts
+++ b/src/mol-util/uuid.ts
@@ -9,6 +9,8 @@ import { now } from 'mol-util/now';
 type UUID = string & { '@type': 'uuid' }
 
 namespace UUID {
+    const _btoa = typeof btoa !== 'undefined' ? btoa : (s: string) => Buffer.from(s).toString('base64')
+
     const chars: string[] = [];
     /** Creates 22 characted "base64" UUID */
     export function create22(): UUID {
@@ -17,7 +19,7 @@ namespace UUID {
             chars[i] = String.fromCharCode((d + Math.random()*0xff)%0xff | 0);
             d = Math.floor(d/0xff);
         }
-        return btoa(chars.join('')).replace(/\+/g, '-').replace(/\//g, '_').substr(0, 22) as UUID;
+        return _btoa(chars.join('')).replace(/\+/g, '-').replace(/\//g, '_').substr(0, 22) as UUID;
     }
 
     export function createv4(): UUID {
diff --git a/src/perf-tests/cif-encoder.ts b/src/perf-tests/cif-encoder.ts
index 59f0eb3f62ebb8bd08bd23b26d9c09d7fae96f57..5749ba5a4f1cdf503dfe40e66baeea35a428bbef 100644
--- a/src/perf-tests/cif-encoder.ts
+++ b/src/perf-tests/cif-encoder.ts
@@ -60,7 +60,7 @@ function testBinary() {
     enc.writeCategory(getCat('cat2'), [{ rowCount: 1, fields: category2fields }]);
     enc.encode();
     const data = enc.getData() as Uint8Array;
-    fs.writeFileSync('e:/test/mol-star/test.bcif', new Buffer(data));
+    fs.writeFileSync('e:/test/mol-star/test.bcif', Buffer.from(data));
     console.log('written binary');
 }
 
diff --git a/src/perf-tests/lookup3d.ts b/src/perf-tests/lookup3d.ts
index 1a18900644164459d29349e5c24156d364281e74..a625a6cd16187429723d8b36b6fbb0e29bc52d7d 100644
--- a/src/perf-tests/lookup3d.ts
+++ b/src/perf-tests/lookup3d.ts
@@ -2,11 +2,12 @@ import * as util from 'util'
 import * as fs from 'fs'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Format } from 'mol-model/structure'
+import { Structure } from 'mol-model/structure'
 
 import { GridLookup3D } from 'mol-math/geometry';
 // import { sortArray } from 'mol-data/util';
 import { OrderedSet } from 'mol-data/int';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 require('util.promisify').shim();
 const readFileAsync = util.promisify(fs.readFile);
@@ -31,11 +32,10 @@ export async function readCIF(path: string) {
         throw parsed;
     }
 
-    const mmcif = Format.mmCIF(parsed.result.blocks[0]);
-    const models = await Model.create(mmcif).run();
+    const models = await trajectoryFromMmCIF(parsed.result.blocks[0]).run();
     const structures = models.map(Structure.ofModel);
 
-    return { mmcif: mmcif.data, models, structures };
+    return { mmcif: models[0].sourceData.data, models, structures };
 }
 
 export async function test() {
diff --git a/src/perf-tests/structure.ts b/src/perf-tests/structure.ts
index 7616ed526dd7b49747d35fdf445acd95830a80f3..48f9e54d5823f01af4f6d529a278513827004bab 100644
--- a/src/perf-tests/structure.ts
+++ b/src/perf-tests/structure.ts
@@ -11,11 +11,12 @@ import * as fs from 'fs'
 import fetch from 'node-fetch'
 import CIF from 'mol-io/reader/cif'
 
-import { Structure, Model, Queries as Q, StructureElement, StructureSelection, StructureSymmetry, StructureQuery, Format, StructureProperties as SP } from 'mol-model/structure'
+import { Structure, Model, Queries as Q, StructureElement, StructureSelection, StructureSymmetry, StructureQuery, StructureProperties as SP } from 'mol-model/structure'
 // import { Segmentation, OrderedSet } from 'mol-data/int'
 
 import to_mmCIF from 'mol-model/structure/export/mmcif'
 import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 // import { printUnits } from 'apps/structure-info/model';
 // import { EquivalenceClasses } from 'mol-data/util';
 
@@ -69,16 +70,12 @@ export async function readCIF(path: string) {
     }
 
     const data = parsed.result.blocks[0];
-    console.time('schema')
-    const mmcif = Format.mmCIF(data);
-
-    console.timeEnd('schema')
     console.time('buildModels')
-    const models = await Model.create(mmcif).run();
+    const models = await trajectoryFromMmCIF(data).run();
     console.timeEnd('buildModels')
     const structures = models.map(Structure.ofModel);
 
-    return { mmcif, models, structures };
+    return { mmcif: models[0].sourceData.data, models, structures };
 }
 
 const DATA_DIR = './build/data';
diff --git a/src/servers/common/swagger-ui/index.ts b/src/servers/common/swagger-ui/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fadda6560e2a9977b850669dbe94a51f84978b0b
--- /dev/null
+++ b/src/servers/common/swagger-ui/index.ts
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as express from 'express'
+import * as fs from 'fs'
+import { getAbsoluteFSPath } from 'swagger-ui-dist'
+import { ServeStaticOptions } from 'serve-static';
+import { interpolate } from 'mol-util/string';
+
+export function swaggerUiAssetsHandler(options?: ServeStaticOptions) {
+    const opts = options || {}
+    opts.index = false
+    return express.static(getAbsoluteFSPath(), opts)
+}
+
+export interface SwaggerUIOptions {
+    openapiJsonUrl: string
+    apiPrefix: string
+    title: string
+    shortcutIconLink: string
+}
+
+function createHTML(options: SwaggerUIOptions) {
+    const htmlTemplate = fs.readFileSync(`${__dirname}/indexTemplate.html`).toString()
+    return interpolate(htmlTemplate, options)
+}
+
+export function swaggerUiIndexHandler(options: SwaggerUIOptions): express.Handler {
+    const html = createHTML(options)
+    return (req: express.Request, res: express.Response) => {
+        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
+        res.end(html);
+    }
+}
\ No newline at end of file
diff --git a/src/servers/common/swagger-ui/indexTemplate.html b/src/servers/common/swagger-ui/indexTemplate.html
new file mode 100644
index 0000000000000000000000000000000000000000..92869e8c5713a1f663fccefd9484562096cae5b1
--- /dev/null
+++ b/src/servers/common/swagger-ui/indexTemplate.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>${title}</title>
+        <link rel="stylesheet" type="text/css" href="${apiPrefix}/swagger-ui.css" >
+        ${shortcutIconLink}
+
+        <style>
+            html
+            {
+                box-sizing: border-box;
+                overflow: -moz-scrollbars-vertical;
+                overflow-y: scroll;
+            }
+            *,
+            *:before,
+            *:after
+            {
+                box-sizing: inherit;
+            }
+            body
+            {
+                margin:0;
+                background: #fafafa;
+            }
+        </style>
+    </head>
+
+    <body>
+        <div id="swagger-ui"></div>
+
+        <script src="${apiPrefix}/swagger-ui-bundle.js"> </script>
+        <script src="${apiPrefix}/swagger-ui-standalone-preset.js"> </script>
+        <script>
+            function HidePlugin() {
+                // this plugin overrides some components to return nothing
+                return {
+                    components: {
+                        Topbar: function () { return null },
+                        Models: function () { return null },
+                    }
+                }
+            }
+            window.onload = function () {
+                var ui = SwaggerUIBundle({
+                    url: '${openapiJsonUrl}',
+                    validatorUrl: null,
+                    docExpansion: 'list',
+                    dom_id: '#swagger-ui',
+                    deepLinking: true,
+                    presets: [
+                        SwaggerUIBundle.presets.apis,
+                        SwaggerUIStandalonePreset
+                    ],
+                    plugins: [
+                        SwaggerUIBundle.plugins.DownloadUrl,
+                        HidePlugin
+                    ],
+                    layout: 'StandaloneLayout'
+                })
+                window.ui = ui
+            }
+        </script>
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/servers/model/preprocess/master.ts b/src/servers/model/preprocess/master.ts
index 7dbb2ba39f368911653815b50ec82adbbdf8bedf..9ff8b8e1fbb321579029f40963ae563aba7f502a 100644
--- a/src/servers/model/preprocess/master.ts
+++ b/src/servers/model/preprocess/master.ts
@@ -26,6 +26,7 @@ cmdParser.addArgument(['--folderNumProcesses', '-fp'], { help: 'Convert folder n
 
 interface CmdArgs {
     // bulk?: string,
+    help?: any,
     cfg?: string,
     input?: string,
     outCIF?: string,
@@ -36,6 +37,7 @@ interface CmdArgs {
     folderNumProcesses?: string
 }
 
+
 export interface PreprocessConfig {
     numProcesses?: number,
     customProperties?: ModelPropertyProviderConfig | string
@@ -43,8 +45,13 @@ export interface PreprocessConfig {
 
 const cmdArgs = cmdParser.parseArgs() as CmdArgs;
 
+if (Object.keys(cmdArgs).filter(k => (cmdArgs as any)[k] !== null).length === 0 || typeof cmdArgs.help !== 'undefined') {
+    cmdParser.printHelp();
+    process.exit(0);
+}
+
 let entries: PreprocessEntry[] = []
-let config: PreprocessConfig = { numProcesses: 1, customProperties: void 0 }
+let config: PreprocessConfig = { numProcesses: cmdArgs.folderIn ? +(cmdArgs.folderNumProcesses || 1) : 1, customProperties: void 0 }
 
 if (cmdArgs.input) entries.push({ source: cmdArgs.input, cif: cmdArgs.outCIF, bcif: cmdArgs.outBCIF });
 // else if (cmdArgs.bulk) runBulk(cmdArgs.bulk);
diff --git a/src/servers/model/preprocess/preprocess.ts b/src/servers/model/preprocess/preprocess.ts
index 187a017b762e0c4bab1993e07ef2d54dc3404604..f36ee59ee64122cdf2f856a7c58960dbec67e06d 100644
--- a/src/servers/model/preprocess/preprocess.ts
+++ b/src/servers/model/preprocess/preprocess.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { readStructureWrapper, resolveStructures } from '../server/structure-wrapper';
+import { readStructureWrapper, resolveStructures, readDataAndFrame } from '../server/structure-wrapper';
 import { classifyCif } from './converter';
 import { Structure } from 'mol-model/structure';
 import { CifWriter } from 'mol-io/writer/cif';
@@ -15,7 +15,14 @@ import { ModelPropertiesProvider } from '../property-provider';
 
 // TODO: error handling
 
-export async function preprocessFile(filename: string, propertyProvider?: ModelPropertiesProvider, outputCif?: string, outputBcif?: string) {
+export function preprocessFile(filename: string, propertyProvider?: ModelPropertiesProvider, outputCif?: string, outputBcif?: string) {
+    return propertyProvider
+        ? preprocess(filename, propertyProvider, outputCif, outputBcif)
+        : convert(filename, outputCif, outputBcif);
+}
+
+
+async function preprocess(filename: string, propertyProvider?: ModelPropertiesProvider, outputCif?: string, outputBcif?: string) {
     const input = await readStructureWrapper('entry', '_local_', filename, propertyProvider);
     const categories = await classifyCif(input.cifFrame);
     const inputStructures = (await resolveStructures(input))!;
@@ -36,6 +43,34 @@ export async function preprocessFile(filename: string, propertyProvider?: ModelP
     }
 }
 
+async function convert(filename: string, outputCif?: string, outputBcif?: string) {
+    const { frame } = await readDataAndFrame(filename);
+    const categories = await classifyCif(frame);
+
+    if (outputCif) {
+        const writer = wrapFileToWriter(outputCif);
+        const encoder = CifWriter.createEncoder({ binary: false });
+        encodeConvert(frame.header, categories, encoder, writer);
+        writer.end();
+    }
+
+    if (outputBcif) {
+        const writer = wrapFileToWriter(outputBcif);
+        const encoder = CifWriter.createEncoder({ binary: true, binaryAutoClassifyEncoding: true });
+        encodeConvert(frame.header, categories, encoder, writer);
+        writer.end();
+    }
+}
+
+function encodeConvert(header: string, categories: CifWriter.Category[], encoder: CifWriter.Encoder, writer: Writer) {
+    encoder.startDataBlock(header);
+    for (const cat of categories) {
+        encoder.writeCategory(cat);
+    }
+    encoder.encode();
+    encoder.writeTo(writer);
+}
+
 function encode(structure: Structure, header: string, categories: CifWriter.Category[], encoder: CifWriter.Encoder, exportCtx: CifExportContext, writer: Writer) {
     const skipCategoryNames = new Set<string>(categories.map(c => c.name));
     encoder.startDataBlock(header);
diff --git a/src/servers/model/property-provider.ts b/src/servers/model/property-provider.ts
index d64e5b4cfbdfd3fd20b10d6d16f844f88715cdc3..77ab34c50f691af4eec5b94f8aef27d5adce1f13 100644
--- a/src/servers/model/property-provider.ts
+++ b/src/servers/model/property-provider.ts
@@ -18,11 +18,11 @@ export type AttachModelProperty = (args: { model: Model, params: any, cache: any
 export type AttachModelProperties = (args: { model: Model, params: any, cache: any }) => Promise<any>[]
 export type ModelPropertiesProvider = (model: Model, cache: any) => Promise<any>[]
 
-export function createModelPropertiesProviderFromConfig(): ModelPropertiesProvider {
+export function createModelPropertiesProviderFromConfig() {
     return createModelPropertiesProvider(Config.customProperties);
 }
 
-export function createModelPropertiesProvider(configOrPath: ModelPropertyProviderConfig | string | undefined): ModelPropertiesProvider {
+export function createModelPropertiesProvider(configOrPath: ModelPropertyProviderConfig | string | undefined): ModelPropertiesProvider | undefined {
     let config: ModelPropertyProviderConfig;
     if (typeof configOrPath === 'string') {
         try {
@@ -35,7 +35,7 @@ export function createModelPropertiesProvider(configOrPath: ModelPropertyProvide
         config = configOrPath!;
     }
 
-    if (!config || !config.sources || config.sources.length === 0) return () => [];
+    if (!config || !config.sources || config.sources.length === 0) return void 0;
 
     const ps: AttachModelProperties[] = [];
     for (const p of config.sources) {
diff --git a/src/servers/model/server/api-local.ts b/src/servers/model/server/api-local.ts
index 83fe022eb141e6fbf4ddeeaa30d15781978b19a4..74178f2c8204b35b48591d995b43eb9fd87e9c05 100644
--- a/src/servers/model/server/api-local.ts
+++ b/src/servers/model/server/api-local.ts
@@ -84,7 +84,7 @@ export function wrapFileToWriter(fn: string) {
         },
         writeBinary(this: any, data: Uint8Array) {
             this.open();
-            fs.writeSync(this.file, new Buffer(data.buffer));
+            fs.writeSync(this.file, Buffer.from(data.buffer));
             return true;
         },
         writeString(this: any, data: string) {
diff --git a/src/servers/model/server/api-web.ts b/src/servers/model/server/api-web.ts
index a141dbfef311fc663fe1c8c6fde3cdbb9007aa20..a472a9f3da3d1f54e2c0658808d16dadc4bd71cc 100644
--- a/src/servers/model/server/api-web.ts
+++ b/src/servers/model/server/api-web.ts
@@ -39,7 +39,7 @@ function wrapResponse(fn: string, res: express.Response) {
         },
         writeBinary(this: any, data: Uint8Array) {
             if (!this.headerWritten) this.writeHeader(true);
-            return res.write(new Buffer(data.buffer));
+            return res.write(Buffer.from(data.buffer));
         },
         writeString(this: any, data: string) {
             if (!this.headerWritten) this.writeHeader(false);
diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts
index f1079596487273caec14432f1fbbc0afc7274063..a9cd4c754a90e9d56ab0c23554feee44b589673e 100644
--- a/src/servers/model/server/query.ts
+++ b/src/servers/model/server/query.ts
@@ -30,7 +30,7 @@ const perf = new PerformanceMonitor();
 let _propertyProvider: ModelPropertiesProvider;
 function propertyProvider() {
     if (_propertyProvider) return _propertyProvider;
-    _propertyProvider = createModelPropertiesProviderFromConfig();
+    _propertyProvider = createModelPropertiesProviderFromConfig() || (() => []);
     return _propertyProvider;
 }
 
@@ -156,7 +156,7 @@ const _model_server_stats_fields: CifField<number, Stats>[] = [
 
 const _model_server_result: CifWriter.Category<Job> = {
     name: 'model_server_result',
-    instance: (job) => CifWriter.categoryInstance(_model_server_result_fields,{ data: job, rowCount: 1 })
+    instance: (job) => CifWriter.categoryInstance(_model_server_result_fields, { data: job, rowCount: 1 })
 };
 
 const _model_server_error: CifWriter.Category<string> = {
diff --git a/src/servers/model/server/structure-wrapper.ts b/src/servers/model/server/structure-wrapper.ts
index 7c8bb1b255ddf845a0c86ac861332e39bd681755..6eef79c02a7d393f4a4a2f1dc3fde726e3e799d0 100644
--- a/src/servers/model/server/structure-wrapper.ts
+++ b/src/servers/model/server/structure-wrapper.ts
@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Structure, Model, Format } from 'mol-model/structure';
+import { Structure, Model } from 'mol-model/structure';
 import { PerformanceMonitor } from 'mol-util/performance-monitor';
 import { Cache } from './cache';
 import Config from '../config';
@@ -15,6 +15,7 @@ import * as zlib from 'zlib'
 import { Job } from './jobs';
 import { ConsoleLogger } from 'mol-util/console-logger';
 import { ModelPropertiesProvider } from '../property-provider';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 require('util.promisify').shim();
 
@@ -89,26 +90,32 @@ async function parseCif(data: string|Uint8Array) {
     return parsed.result;
 }
 
-export async function readStructureWrapper(key: string, sourceId: string | '_local_', entryId: string, propertyProvider: ModelPropertiesProvider | undefined) {
-    const filename = sourceId === '_local_' ? entryId : Config.mapFile(sourceId, entryId);
-    if (!filename) throw new Error(`Cound not map '${key}' to a valid filename.`);
-    if (!fs.existsSync(filename)) throw new Error(`Could not find source file for '${key}'.`);
-
+export async function readDataAndFrame(filename: string, key?: string) {
     perf.start('read');
     let data;
     try {
         data = await readFile(filename);
     } catch (e) {
-        ConsoleLogger.error(key, '' + e);
-        throw new Error(`Could not read the file for '${key}' from disk.`);
+        ConsoleLogger.error(key || filename, '' + e);
+        throw new Error(`Could not read the file for '${key || filename}' from disk.`);
     }
 
     perf.end('read');
     perf.start('parse');
     const frame = (await parseCif(data)).blocks[0];
     perf.end('parse');
+
+    return { data, frame };
+}
+
+export async function readStructureWrapper(key: string, sourceId: string | '_local_', entryId: string, propertyProvider: ModelPropertiesProvider | undefined) {
+    const filename = sourceId === '_local_' ? entryId : Config.mapFile(sourceId, entryId);
+    if (!filename) throw new Error(`Cound not map '${key}' to a valid filename.`);
+    if (!fs.existsSync(filename)) throw new Error(`Could not find source file for '${key}'.`);
+
+    const { data, frame } = await readDataAndFrame(filename, key);
     perf.start('createModel');
-    const models = await Model.create(Format.mmCIF(frame)).run();
+    const models = await trajectoryFromMmCIF(frame).run();
     perf.end('createModel');
 
     const modelMap = new Map<number, Model>();
diff --git a/src/servers/model/test.ts b/src/servers/model/test.ts
index 8d7f85b1e4dcbd354389f57bb6227c346e728608..b250de2ef48f836723eeb758a0c62c1bc08ac847 100644
--- a/src/servers/model/test.ts
+++ b/src/servers/model/test.ts
@@ -13,7 +13,7 @@ function wrapFile(fn: string) {
         },
         writeBinary(this: any, data: Uint8Array) {
             this.open();
-            fs.writeSync(this.file, new Buffer(data));
+            fs.writeSync(this.file, Buffer.from(data));
             return true;
         },
         writeString(this: any, data: string) {
diff --git a/src/servers/volume/common/binary-schema.ts b/src/servers/volume/common/binary-schema.ts
index 8ce1b47007f50177248f128924d75f617a8daf22..cdc72fb51d33c5190dfb511225c11c63538be095 100644
--- a/src/servers/volume/common/binary-schema.ts
+++ b/src/servers/volume/common/binary-schema.ts
@@ -7,6 +7,7 @@
  */
 
 import * as UTF8 from 'mol-io/common/utf8'
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 export type Bool = { kind: 'bool' }
 export type Int = { kind: 'int' }
@@ -96,7 +97,7 @@ function writeElement(e: Element, buffer: Buffer, src: any, offset: number) {
 
 function write(element: Element, src: any) {
     const size = byteCount(element, src);
-    const buffer = new Buffer(size);
+    const buffer = Buffer.alloc(size);
     writeElement(element, buffer, src, 0);
     return buffer;
 }
@@ -105,7 +106,7 @@ export function encode(element: Element, src: any): Buffer {
     return write(element, src);
 }
 
-function decodeElement(e: Element, buffer: Buffer, offset: number, target: { value: any }) {
+function decodeElement(e: Element, buffer: SimpleBuffer, offset: number, target: { value: any }) {
     switch (e.kind) {
         case 'bool': target.value = !!buffer.readInt8(offset); offset += 1; break;
         case 'int': target.value = buffer.readInt32LE(offset); offset += 4; break;
@@ -147,7 +148,7 @@ function decodeElement(e: Element, buffer: Buffer, offset: number, target: { val
     return offset;
 }
 
-export function decode<T>(element: Element, buffer: Buffer, offset?: number) {
+export function decode<T>(element: Element, buffer: SimpleBuffer, offset?: number) {
     const target = { value: void 0 as any };
     decodeElement(element, buffer, offset! | 0, target);
     return target.value as T;
diff --git a/src/servers/volume/common/data-format.ts b/src/servers/volume/common/data-format.ts
index f1fdeda83277dd145831667fdad3dd4f476bd7af..e3a51fe8cc89353ca46d4801c975378f64330d91 100644
--- a/src/servers/volume/common/data-format.ts
+++ b/src/servers/volume/common/data-format.ts
@@ -6,18 +6,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as File from './file'
 import * as Schema from './binary-schema'
-
-export type ValueType = 'float32' | 'int8' | 'int16'
-
-export namespace ValueType {
-    export const Float32: ValueType = 'float32';
-    export const Int8: ValueType = 'int8';
-    export const Int16: ValueType = 'int16';
-}
-
-export type ValueArray = Float32Array | Int8Array | Int16Array
+import { FileHandle } from 'mol-io/common/file-handle';
+import { TypedArrayValueType } from 'mol-io/common/typed-array';
 
 export interface Spacegroup {
     number: number,
@@ -41,7 +32,7 @@ export interface Sampling {
     rate: number,
     valuesInfo: ValuesInfo[],
 
-    /** Number of samples along each axis, in axisOrder  */
+    /** Number of samples along each axis, in axisOrder */
     sampleCount: number[]
 }
 
@@ -62,7 +53,7 @@ export interface Header {
     channels: string[],
 
     /** Determines the data type of the values */
-    valueType: ValueType,
+    valueType: TypedArrayValueType,
 
     /** The value are stored in blockSize^3 cubes */
     blockSize: number,
@@ -102,31 +93,16 @@ namespace _schema {
 
 const headerSchema = _schema.schema;
 
-export function getValueByteSize(type: ValueType) {
-    if (type === ValueType.Float32) return 4;
-    if (type === ValueType.Int16) return 2;
-    return 1;
-}
-
-export function createValueArray(type: ValueType, size: number) {
-    switch (type) {
-        case ValueType.Float32: return new Float32Array(new ArrayBuffer(4 * size));
-        case ValueType.Int8: return new Int8Array(new ArrayBuffer(1 * size));
-        case ValueType.Int16: return new Int16Array(new ArrayBuffer(2 * size));
-    }
-    throw Error(`${type} is not a supported value format.`);
-}
-
 export function encodeHeader(header: Header) {
     return Schema.encode(headerSchema, header);
 }
 
-export async function readHeader(file: number): Promise<{ header: Header, dataOffset: number }> {
-    let { buffer } = await File.readBuffer(file, 0, 4 * 4096);
+export async function readHeader(file: FileHandle): Promise<{ header: Header, dataOffset: number }> {
+    let { buffer } = await file.readBuffer(0, 4 * 4096);
     const headerSize = buffer.readInt32LE(0);
 
     if (headerSize > buffer.byteLength - 4) {
-        buffer = (await File.readBuffer(file, 0, headerSize + 4)).buffer;
+        buffer = (await file.readBuffer(0, headerSize + 4)).buffer;
     }
 
     const header = Schema.decode<Header>(headerSchema, buffer, 4);
diff --git a/src/servers/volume/common/file.ts b/src/servers/volume/common/file.ts
index 6b83f00cef7e225de8dde078e257698fc17eb162..f641faf7e1c47d393f80f1c12389c652ec3f1d35 100644
--- a/src/servers/volume/common/file.ts
+++ b/src/servers/volume/common/file.ts
@@ -8,9 +8,8 @@
 
 import * as fs from 'fs'
 import * as path from 'path'
-import * as DataFormat from './data-format'
-
-export const IsNativeEndianLittle = new Uint16Array(new Uint8Array([0x12, 0x34]).buffer)[0] === 0x3412;
+import { FileHandle } from 'mol-io/common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 export async function openRead(filename: string) {
     return new Promise<number>((res, rej) => {
@@ -29,43 +28,6 @@ export async function openRead(filename: string) {
     });
 }
 
-export function readBuffer(file: number, position: number, sizeOrBuffer: Buffer | number, size?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: Buffer }> {
-    return new Promise((res, rej) => {
-        if (typeof sizeOrBuffer === 'number') {
-            let buff = new Buffer(new ArrayBuffer(sizeOrBuffer));
-            fs.read(file, buff, 0, sizeOrBuffer, position, (err, bytesRead, buffer) => {
-                if (err) {
-                    rej(err);
-                    return;
-                }
-                res({ bytesRead, buffer });
-            });
-        } else {
-            if (size === void 0) {
-                rej('readBuffer: Specify size.');
-                return;
-            }
-
-            fs.read(file, sizeOrBuffer, byteOffset ? +byteOffset : 0, size, position, (err, bytesRead, buffer) => {
-                if (err) {
-                    rej(err);
-                    return;
-                }
-                res({ bytesRead, buffer });
-            });
-        }
-    })
-}
-
-export function writeBuffer(file: number, position: number, buffer: Buffer, size?: number): Promise<number> {
-    return new Promise<number>((res, rej) => {
-        fs.write(file, buffer, 0, size !== void 0 ? size : buffer.length, position, (err, written) => {
-            if (err) rej(err);
-            else res(written);
-        })
-    })
-}
-
 function makeDir(path: string, root?: string): boolean {
     let dirs = path.split(/\/|\\/g),
         dir = dirs.shift();
@@ -95,77 +57,8 @@ export function createFile(filename: string) {
     });
 }
 
-const __emptyFunc = function () { };
-export function close(file: number | undefined) {
-    try {
-        if (file !== void 0) fs.close(file, __emptyFunc);
-    } catch (e) {
-
-    }
-}
-
-const smallBuffer = new Buffer(8);
-export async function writeInt(file: number, value: number, position: number) {
+const smallBuffer = SimpleBuffer.fromBuffer(Buffer.alloc(8));
+export async function writeInt(file: FileHandle, value: number, position: number) {
     smallBuffer.writeInt32LE(value, 0);
-    await writeBuffer(file, position, smallBuffer, 4);
-}
-
-export interface TypedArrayBufferContext {
-    type: DataFormat.ValueType,
-    elementByteSize: number,
-    readBuffer: Buffer,
-    valuesBuffer: Uint8Array,
-    values: DataFormat.ValueArray
-}
-
-function getElementByteSize(type: DataFormat.ValueType) {
-    if (type === DataFormat.ValueType.Float32) return 4;
-    if (type === DataFormat.ValueType.Int16) return 2;
-    return 1;
-}
-
-function makeTypedArray(type: DataFormat.ValueType, buffer: ArrayBuffer): DataFormat.ValueArray {
-    if (type === DataFormat.ValueType.Float32) return new Float32Array(buffer);
-    if (type === DataFormat.ValueType.Int16) return new Int16Array(buffer);
-    return new Int8Array(buffer);
-}
-
-export function createTypedArrayBufferContext(size: number, type: DataFormat.ValueType): TypedArrayBufferContext {
-    let elementByteSize = getElementByteSize(type);
-    let arrayBuffer = new ArrayBuffer(elementByteSize * size);
-    let readBuffer = new Buffer(arrayBuffer);
-    let valuesBuffer = IsNativeEndianLittle ? arrayBuffer : new ArrayBuffer(elementByteSize * size);
-    return {
-        type,
-        elementByteSize,
-        readBuffer,
-        valuesBuffer: new Uint8Array(valuesBuffer),
-        values: makeTypedArray(type, valuesBuffer)
-    };
-}
-
-function flipByteOrder(source: Buffer, 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 async function readTypedArray(ctx: TypedArrayBufferContext, file: number, position: number, count: number, valueOffset: number, littleEndian?: boolean) {
-    let byteCount = ctx.elementByteSize * count;
-    let byteOffset = ctx.elementByteSize * valueOffset;
-
-    await readBuffer(file, position, ctx.readBuffer, byteCount, byteOffset);
-    if (ctx.elementByteSize > 1 && ((littleEndian !== void 0 && littleEndian !== IsNativeEndianLittle) || !IsNativeEndianLittle)) {
-        // fix the endian 
-        flipByteOrder(ctx.readBuffer, ctx.valuesBuffer, byteCount, ctx.elementByteSize, byteOffset);
-    }
-    return ctx.values;
-}
-
-export function ensureLittleEndian(source: Buffer, target: Buffer, byteCount: number, elementByteSize: number, offset: number) {
-    if (IsNativeEndianLittle) return;
-    if (!byteCount || elementByteSize <= 1) return;
-    flipByteOrder(source, target, byteCount, elementByteSize, offset);
+    await file.writeBuffer(position, smallBuffer, 4);
 }
\ No newline at end of file
diff --git a/src/servers/volume/config.ts b/src/servers/volume/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..236c1c7ba32beead71a5d9e7cf49e1eeb394fa42
--- /dev/null
+++ b/src/servers/volume/config.ts
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as argparse from 'argparse'
+
+export function addLimitsArgs(parser: argparse.ArgumentParser) {
+    parser.addArgument([ '--maxRequestBlockCount' ], {
+        defaultValue: DefaultLimitsConfig.maxRequestBlockCount,
+        metavar: 'COUNT',
+        help: `Maximum number of blocks that could be read in 1 query.
+This is somewhat tied to the maxOutputSizeInVoxelCountByPrecisionLevel
+in that the <maximum number of voxel> = maxRequestBlockCount * <block size>^3.
+The default block size is 96 which corresponds to 28,311,552 voxels with 32 max blocks.`
+    });
+    parser.addArgument([ '--maxFractionalBoxVolume' ], {
+        defaultValue: DefaultLimitsConfig.maxFractionalBoxVolume,
+        metavar: 'VOLUME',
+        help: `The maximum fractional volume of the query box (to prevent queries that are too big).`
+    });
+    parser.addArgument([ '--maxOutputSizeInVoxelCountByPrecisionLevel' ], {
+        nargs: '+',
+        defaultValue: DefaultLimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel,
+        metavar: 'LEVEL',
+        help: `What is the (approximate) maximum desired size in voxel count by precision level
+Rule of thumb: <response gzipped size> in [<voxel count> / 8, <voxel count> / 4].
+The maximum number of voxels is tied to maxRequestBlockCount.`
+    });
+}
+
+export function addServerArgs(parser: argparse.ArgumentParser) {
+    parser.addArgument([ '--apiPrefix' ], {
+        defaultValue: DefaultServerConfig.apiPrefix,
+        metavar: 'PREFIX',
+        help: `Specify the prefix of the API, i.e. <host>/<apiPrefix>/<API queries>`
+    });
+    parser.addArgument([ '--defaultPort' ], {
+        defaultValue: DefaultServerConfig.defaultPort,
+        metavar: 'PORT',
+        help: `Specify the prefix of the API, i.e. <host>/<apiPrefix>/<API queries>`
+    });
+
+    parser.addArgument([ '--shutdownTimeoutMinutes' ], {
+        defaultValue: DefaultServerConfig.shutdownTimeoutMinutes,
+        metavar: 'TIME',
+        help: `0 for off, server will shut down after this amount of minutes.`
+    });
+    parser.addArgument([ '--shutdownTimeoutVarianceMinutes' ], {
+        defaultValue: DefaultServerConfig.shutdownTimeoutVarianceMinutes,
+        metavar: 'VARIANCE',
+        help: `modifies the shutdown timer by +/- timeoutVarianceMinutes (to avoid multiple instances shutting at the same time)`
+    });
+    parser.addArgument([ '--idMap' ], {
+        nargs: 2,
+        action: 'append',
+        metavar: ['TYPE', 'PATH'] as any,
+        help: [
+            'Map `id`s for a `type` to a file path.',
+            'Example: x-ray \'../../data/mdb/xray/${id}-ccp4.mdb\'',
+            'Note: Can be specified multiple times.'
+        ].join('\n'),
+    });
+}
+
+const DefaultServerConfig = {
+    apiPrefix: '/VolumeServer',
+    defaultPort: 1337,
+    shutdownTimeoutMinutes: 24 * 60, /* a day */
+    shutdownTimeoutVarianceMinutes: 60,
+    idMap: [] as [string, string][]
+}
+export type ServerConfig = typeof DefaultServerConfig
+export const ServerConfig = { ...DefaultServerConfig }
+export function setServerConfig(config: ServerConfig) {
+    for (const name in DefaultServerConfig) {
+        ServerConfig[name as keyof ServerConfig] = config[name as keyof ServerConfig]
+    }
+}
+
+const DefaultLimitsConfig = {
+    maxRequestBlockCount: 32,
+    maxFractionalBoxVolume: 1024,
+    maxOutputSizeInVoxelCountByPrecisionLevel: [
+        0.5 * 1024 * 1024, // ~ 80*80*80
+        1 * 1024 * 1024,
+        2 * 1024 * 1024,
+        4 * 1024 * 1024,
+        8 * 1024 * 1024,
+        16 * 1024 * 1024, // ~ 256*256*256
+        24 * 1024 * 1024
+    ]
+}
+export type LimitsConfig = typeof DefaultLimitsConfig
+export const LimitsConfig = { ...DefaultLimitsConfig }
+export function setLimitsConfig(config: LimitsConfig) {
+    for (const name in DefaultLimitsConfig) {
+        LimitsConfig[name as keyof LimitsConfig] = config[name as keyof LimitsConfig]
+    }
+}
+
+export function setConfig(config: ServerConfig & LimitsConfig) {
+    setServerConfig(config)
+    setLimitsConfig(config)
+}
\ No newline at end of file
diff --git a/src/servers/volume/local.ts b/src/servers/volume/local.ts
index 45e379769edf45f3c8d9c303b2d6847010f2d17d..99993cfbcfa10612463f83325ff959db7cd93f43 100644
--- a/src/servers/volume/local.ts
+++ b/src/servers/volume/local.ts
@@ -1,17 +1,19 @@
 /**
- * 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.
  *
  * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import * as argparse from 'argparse'
 import * as LocalApi from './server/local-api'
 import VERSION from './server/version'
-
 import * as fs from 'fs'
+import { LimitsConfig, addLimitsArgs, setLimitsConfig } from './config';
 
-console.log(`VolumeServer ${VERSION}, (c) 2016 - now, David Sehnal`);
+console.log(`VolumeServer Local ${VERSION}, (c) 2018-2019, Mol* contributors`);
 console.log();
 
 function help() {
@@ -48,21 +50,25 @@ function help() {
         outputFolder: 'g:/test/local-test'
     }];
 
-    console.log('Usage: node local jobs.json');
-    console.log();
-    console.log('Example jobs.json:');
-    console.log(JSON.stringify(exampleJobs, null, 2));
+    return `Usage: node local jobs.json\n\nExample jobs.json: ${JSON.stringify(exampleJobs, null, 2)}`
 }
 
-async function run() {
-    if (process.argv.length !== 3) {
-        help();
-        return;
-    }
+const parser = new argparse.ArgumentParser({
+    addHelp: true,
+    description: help()
+});
+addLimitsArgs(parser)
+parser.addArgument(['jobs'], {
+    help: `Path to jobs JSON file.`
+})
+
+const config: LimitsConfig & { jobs: string } = parser.parseArgs()
+setLimitsConfig(config) // sets the config for global use
 
+async function run() {
     let jobs: LocalApi.JobEntry[];
     try {
-        jobs = JSON.parse(fs.readFileSync(process.argv[2], 'utf-8'));
+        jobs = JSON.parse(fs.readFileSync(config.jobs, 'utf-8'));
     } catch (e) {
         console.log('Error:');
         console.error(e);
diff --git a/src/servers/volume/pack.ts b/src/servers/volume/pack.ts
index 47fb39c988ec8488df1ff0fa606924a9becf57ab..b00d95c5e22b5eab39125e90eb1b1ba0796ea4a0 100644
--- a/src/servers/volume/pack.ts
+++ b/src/servers/volume/pack.ts
@@ -1,86 +1,92 @@
 /**
- * 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.
  *
  * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import * as argparse from 'argparse'
 import pack from './pack/main'
 import VERSION from './pack/version'
 
-let config = {
-    input: <{ name: string, filename: string }[]>[],
-    isPeriodic: false,
-    outputFilename: '',
-    blockSize: 96
-};
+type FileFormat = 'ccp4' | 'dsn6'
 
-function printHelp() {
-    let help = [
-        `VolumeServer Packer ${VERSION}, (c) 2016 - now, David Sehnal`,
-        ``,
-        `The input data must be CCP4/MAP mode 2 (32-bit floats) files.`,
-        ``,
-        `Usage: `,
-        ``,
-        `  node pack -v`,
-        `    Print version.`,
-        ``,
-        `  node pack -xray main.ccp4 diff.ccp4 output.mdb [-blockSize 96]`,
-        `    Pack main and diff density into a single block file.`,
-        `    Optionally specify maximum block size.`,
-        ``,
-        `  node pack -em density.map output.mdb [-blockSize 96]`,
-        `    Pack single density into a block file.`, 
-        `    Optionally specify maximum block size.`
-    ];
-    console.log(help.join('\n'));
+interface Config {
+    input: { name: string, filename: string }[],
+    format: FileFormat,
+    isPeriodic: boolean,
+    outputFilename: string,
+    blockSizeInMB: number
 }
 
-function parseInput() {
-    let input = false;
-
-    if (process.argv.length <= 2) {
-        printHelp();
-        process.exit();
-        return false;
+function getConfig(args: Args) {
+    const config: Partial<Config> = {
+        blockSizeInMB: args.blockSizeInMB,
+        format: args.format,
+        outputFilename: args.output
     }
-
-    for (let i = 2; i < process.argv.length; i++) {
-        switch (process.argv[i].toLowerCase()) {
-            case '-blocksize':
-                config.blockSize = +process.argv[++i];
-                break;
-            case '-xray':
-                input = true;
-                config.input = [
-                    { name: '2Fo-Fc', filename: process.argv[++i] },
-                    { name: 'Fo-Fc', filename: process.argv[++i] }
-                ];
-                config.isPeriodic = true;
-                config.outputFilename = process.argv[++i];
-                break;
-            case '-em':
-                input = true;
-                config.input = [
-                    { name: 'em', filename: process.argv[++i] }
-                ];
-                config.outputFilename = process.argv[++i];
-                break;
-            case '-v':
-                console.log(VERSION);
-                process.exit();
-                return false;
-            default:
-                printHelp();
-                process.exit();
-                return false;
-        }
+    switch (args.mode) {
+        case 'em':
+            config.input = [
+                { name: 'em', filename: args.inputEm }
+            ];
+            config.isPeriodic = false;
+            break
+        case 'xray':
+            config.input = [
+                { name: '2Fo-Fc', filename: args.input2fofc },
+                { name: 'Fo-Fc', filename: args.inputFofc }
+            ];
+            config.isPeriodic = true;
+            break
     }
-    return input;
+    return config as Config
+}
+
+interface GeneralArgs {
+    blockSizeInMB: number
+    format: FileFormat
+    output: string
 }
+interface XrayArgs extends GeneralArgs {
+    mode: 'xray'
+    input2fofc: string
+    inputFofc: string
+}
+interface EmArgs extends GeneralArgs {
+    mode: 'em'
+    inputEm: string
+}
+type Args = XrayArgs | EmArgs
+
+const parser = new argparse.ArgumentParser({
+    addHelp: true,
+    description: `VolumeServer Packer ${VERSION}, (c) 2018-2019, Mol* contributors`
+});
+
+const subparsers = parser.addSubparsers({
+    title: 'Packing modes',
+    dest: 'mode'
+});
+
+function addGeneralArgs(parser: argparse.ArgumentParser) {
+    parser.addArgument(['output'], { help: `Output path.` })
+    parser.addArgument(['--blockSizeInMB'], { defaultValue: 96, help: `Maximum block size.`, metavar: 'SIZE' })
+    parser.addArgument(['--format'], { defaultValue: 'ccp4', help: `Input file format.` })
+}
+
+const xrayParser = subparsers.addParser('xray', { addHelp: true })
+xrayParser.addArgument(['input2fofc'], { help: `Path to 2fofc file.`, metavar: '2FOFC' })
+xrayParser.addArgument(['inputFofc'], { help: `Path to fofc file.`, metavar: 'FOFC' })
+addGeneralArgs(xrayParser)
+
+const emParser = subparsers.addParser('em', { addHelp: true })
+emParser.addArgument(['inputEm'], { help: `Path to EM density file.`, metavar: 'EM' })
+addGeneralArgs(emParser)
+
+const args: Args = parser.parseArgs();
+const config = getConfig(args)
 
-if (parseInput()) {
-    pack(config.input, config.blockSize, config.isPeriodic, config.outputFilename);
-}
\ No newline at end of file
+pack(config.input, config.blockSizeInMB, config.isPeriodic, config.outputFilename, config.format);
diff --git a/src/servers/volume/pack/ccp4.ts b/src/servers/volume/pack/ccp4.ts
deleted file mode 100644
index 46ac835bc1d1f6dd33d749d2a0458e8071e41657..0000000000000000000000000000000000000000
--- a/src/servers/volume/pack/ccp4.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * Copyright (c) 2018 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>
- */
-
-import * as File from '../common/file'
-import * as DataFormat from '../common/data-format'
-
-export const enum Mode { Int8 = 0, Int16 = 1, Float32 = 2 }
-
-export interface Header {
-    name: string,
-    mode: Mode,
-    grid: number[], // grid is converted to the axis order!!
-    axisOrder: number[],
-    extent: number[],
-    origin: number[],
-    spacegroupNumber: number,
-    cellSize: number[],
-    cellAngles: number[],
-    littleEndian: boolean,
-    dataOffset: number
-}
-
-/** Represents a circular buffer for 2 * blockSize layers */
-export interface SliceBuffer {
-    buffer: File.TypedArrayBufferContext,
-    sliceCapacity: number,
-    slicesRead: number,
-
-    values: DataFormat.ValueArray,
-    sliceCount: number,
-
-    /** Have all the input slice been read? */
-    isFinished: boolean
-}
-
-export interface Data {
-    header: Header,
-    file: number,
-    slices: SliceBuffer
-}
-
-export function getValueType(header: Header) {
-    if (header.mode === Mode.Float32) return DataFormat.ValueType.Float32;
-    if (header.mode === Mode.Int16) return DataFormat.ValueType.Int16;
-    return DataFormat.ValueType.Int8;
-}
-
-export function assignSliceBuffer(data: Data, blockSize: number) {
-    const { extent } = data.header;
-    const valueType = getValueType(data.header);
-    const sliceSize = extent[0] * extent[1] * DataFormat.getValueByteSize(valueType);
-    const sliceCapacity = Math.max(1, Math.floor(Math.min(64 * 1024 * 1024, sliceSize * extent[2]) / sliceSize));
-    const buffer = File.createTypedArrayBufferContext(sliceCapacity * extent[0] * extent[1], valueType);
-    data.slices = {
-        buffer,
-        sliceCapacity,
-        slicesRead: 0,
-        values: buffer.values,
-        sliceCount: 0,
-        isFinished: false
-    };
-}
-
-function compareProp(a: any, b: any) {
-    if (a instanceof Array && b instanceof Array) {
-        if (a.length !== b.length) return false;
-        for (let i = 0; i < a.length; i++) {
-            if (a[i] !== b[i]) return false;
-        }
-        return true;
-    }
-    return a === b;
-}
-
-export function compareHeaders(a: Header, b: Header) {
-    for (const p of ['grid', 'axisOrder', 'extent', 'origin', 'spacegroupNumber', 'cellSize', 'cellAngles', 'mode']) {
-        if (!compareProp((a as any)[p], (b as any)[p])) return false;
-    }
-    return true;
-}
-
-function getArray(r: (offset: number) => number, offset: number, count: number) {
-    const ret: number[] = [];
-    for (let i = 0; i < count; i++) {
-        ret[i] = r(offset + i);
-    }
-    return ret;
-}
-
-async function readHeader(name: string, file: number) {
-    const headerSize = 1024;
-    const { buffer: data } = await File.readBuffer(file, 0, headerSize);
-
-    let littleEndian = true;
-
-    let mode = data.readInt32LE(3 * 4);
-    if (mode < 0 || mode > 2) {
-        littleEndian = false;
-        mode = data.readInt32BE(3 * 4, true);
-        if (mode < 0 || mode > 2) {
-            throw Error('Only CCP4 modes 0, 1, and 2 are supported.');
-        }
-    }
-
-    const readInt = littleEndian ? (o: number) => data.readInt32LE(o * 4) : (o: number) => data.readInt32BE(o * 4);
-    const readFloat = littleEndian ? (o: number) => data.readFloatLE(o * 4) : (o: number) => data.readFloatBE(o * 4);
-
-    const origin2k = getArray(readFloat, 49, 3);
-    const nxyzStart = getArray(readInt, 4, 3);
-    const header: Header = {
-        name,
-        mode,
-        grid: getArray(readInt, 7, 3),
-        axisOrder: getArray(readInt, 16, 3).map(i => i - 1),
-        extent: getArray(readInt, 0, 3),
-        origin: origin2k[0] === 0.0 && origin2k[1] === 0.0 && origin2k[2] === 0.0 ? nxyzStart : origin2k,
-        spacegroupNumber: readInt(22),
-        cellSize: getArray(readFloat, 10, 3),
-        cellAngles: getArray(readFloat, 13, 3),
-        // mean: readFloat(21),
-        littleEndian,
-        dataOffset: headerSize + readInt(23) /* symBytes */
-    };
-    // "normalize" the grid axis order
-    header.grid = [header.grid[header.axisOrder[0]], header.grid[header.axisOrder[1]], header.grid[header.axisOrder[2]]];
-    return header;
-}
-
-export async function readSlices(data: Data) {
-    const { slices, header } = data;
-    if (slices.isFinished) {
-        return;
-    }
-
-    const { extent } = header;
-    const sliceSize = extent[0] * extent[1];
-    const sliceByteOffset = slices.buffer.elementByteSize * sliceSize * slices.slicesRead;
-    const sliceCount = Math.min(slices.sliceCapacity, extent[2] - slices.slicesRead);
-    const sliceByteCount = sliceCount * sliceSize;
-
-    await File.readTypedArray(slices.buffer, data.file, header.dataOffset + sliceByteOffset, sliceByteCount, 0, header.littleEndian);
-    slices.slicesRead += sliceCount;
-    slices.sliceCount = sliceCount;
-
-    if (slices.slicesRead >= extent[2]) {
-        slices.isFinished = true;
-    }
-}
-
-export async function open(name: string, filename: string): Promise<Data> {
-    const file = await File.openRead(filename);
-    const header = await readHeader(name, file);
-    return {
-        header,
-        file,
-        slices: void 0 as any
-    };
-}
\ No newline at end of file
diff --git a/src/servers/volume/pack/data-model.ts b/src/servers/volume/pack/data-model.ts
index 268fdeb5cd9dbb43f7d194f2f4a0d08e069085b8..9e80f0f80be0cb6a1a4ed76546aed775e4b56dd0 100644
--- a/src/servers/volume/pack/data-model.ts
+++ b/src/servers/volume/pack/data-model.ts
@@ -5,8 +5,11 @@
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
-import * as CCP4 from './ccp4'
+import * as Format from './format'
 import * as DataFormat from '../common/data-format'
+import { FileHandle } from 'mol-io/common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+import { TypedArrayValueArray, TypedArrayValueType } from 'mol-io/common/typed-array';
 
 const FORMAT_VERSION = '1.0.0';
 
@@ -23,16 +26,16 @@ export interface ValuesInfo {
 }
 
 export interface BlockBuffer {
-    values: DataFormat.ValueArray[],
-    buffers: Buffer[],
+    values: TypedArrayValueArray[],
+    buffers: SimpleBuffer[],
     slicesWritten: number
 }
 
 export interface DownsamplingBuffer {
     /** dimensions (sampleCount[1], sampleCount[0] / 2, 1), axis order (K, H, L) */
-    downsampleH: DataFormat.ValueArray,
+    downsampleH: TypedArrayValueArray,
     /** "Cyclic" (in the 1st dimensions) buffer with dimensions (5, sampleCount[0] / 2, sampleCount[1] / 2), axis order (L, H, K),  */
-    downsampleHK: DataFormat.ValueArray,
+    downsampleHK: TypedArrayValueArray,
 
     slicesWritten: number,
     startSliceIndex: number
@@ -68,18 +71,18 @@ export interface Kernel {
 
 export interface Context {
     /** Output file handle  */
-    file: number,
+    file: FileHandle,
 
     /** Periodic are x-ray density files that cover the entire grid and have [0,0,0] origin */
     isPeriodic: boolean,
 
-    channels: CCP4.Data[],
-    valueType: DataFormat.ValueType,
+    channels: Format.Context[],
+    valueType: TypedArrayValueType,
     blockSize: number,
     /** Able to store channels.length * blockSize^3 values. */
-    cubeBuffer: Buffer,
+    cubeBuffer: SimpleBuffer,
     /** All values are stored in little endian format which might not be the native endian of the system  */
-    litteEndianCubeBuffer: Buffer,
+    litteEndianCubeBuffer: SimpleBuffer,
 
     kernel: Kernel,
     sampling: Sampling[],
@@ -90,7 +93,7 @@ export interface Context {
 }
 
 export function createHeader(ctx: Context): DataFormat.Header {
-    const header = ctx.channels[0].header;
+    const header = ctx.channels[0].data.header;
     const grid = header.grid;
 
     function normalize(data: number[]) {
@@ -99,13 +102,13 @@ export function createHeader(ctx: Context): DataFormat.Header {
 
     return {
         formatVersion: FORMAT_VERSION,
-        valueType: CCP4.getValueType(header),
+        valueType: header.valueType,
         blockSize: ctx.blockSize,
         axisOrder: header.axisOrder,
         origin: normalize(header.origin),
         dimensions: normalize(header.extent),
         spacegroup: { number: header.spacegroupNumber, size: header.cellSize, angles: header.cellAngles, isPeriodic: ctx.isPeriodic },
-        channels: ctx.channels.map(c => c.header.name),
+        channels: ctx.channels.map(c => c.data.header.name),
         sampling: ctx.sampling.map(s => {
             const N = s.sampleCount[0] * s.sampleCount[1] * s.sampleCount[2];
             const valuesInfo = [];
diff --git a/src/servers/volume/pack/downsampling.ts b/src/servers/volume/pack/downsampling.ts
index 5541b8a9a120f2f26a975cfa00a6915e95a32a48..25f67d795e5676a6f6ebe026d5f73fc0383db6f6 100644
--- a/src/servers/volume/pack/downsampling.ts
+++ b/src/servers/volume/pack/downsampling.ts
@@ -7,10 +7,10 @@
  */
 
 import * as Data from './data-model'
-import * as DataFormat from '../common/data-format'
+import { TypedArrayValueArray } from 'mol-io/common/typed-array';
 
-/** 
- * Downsamples each slice of input data and checks if there is enough data to perform 
+/**
+ * Downsamples each slice of input data and checks if there is enough data to perform
  * higher rate downsampling.
  */
 export function downsampleLayer(ctx: Data.Context) {
@@ -25,8 +25,8 @@ export function downsampleLayer(ctx: Data.Context) {
     }
 }
 
-/** 
- * When the "native" (rate = 1) sampling is finished, there might still 
+/**
+ * When the "native" (rate = 1) sampling is finished, there might still
  * be some data left to be processed for the higher rate samplings.
  */
 export function finalize(ctx: Data.Context) {
@@ -46,7 +46,7 @@ export function finalize(ctx: Data.Context) {
 /**
  * The functions downsampleH and downsampleHK both essentially do the
  * same thing: downsample along H (1st axis in axis order) and K (2nd axis in axis order) axes respectively.
- * 
+ *
  * The reason there are two copies of almost the same code is performance:
  * Both functions use a different memory layout to improve cache coherency
  *  - downsampleU uses the H axis as the fastest moving one
@@ -54,7 +54,7 @@ export function finalize(ctx: Data.Context) {
  */
 
 
-function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) {
+function conv(w: number, c: number[], src: TypedArrayValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) {
     return w * (c[0] * src[b + i0] + c[1] * src[b + i1] + c[2] * src[b + i2] + c[3] * src[b + i3] + c[4] * src[b + i4]);
 }
 
@@ -63,7 +63,7 @@ function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0:
  * flipping the 1st and 2nd axis in the process to optimize cache coherency for downsampleUV call
  * (i.e. use (K, H, L) axis order).
  */
-function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.ValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) {
+function downsampleH(kernel: Data.Kernel, srcDims: number[], src: TypedArrayValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) {
     const target = buffer.downsampleH;
     const sizeH = srcDims[0], sizeK = srcDims[1], srcBaseOffset = srcLOffset * sizeH * sizeK;
     const targetH = Math.floor((sizeH + 1) / 2);
@@ -87,9 +87,9 @@ function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.Val
     }
 }
 
-/** 
- * Downsample first axis in the slice present in buffer.downsampleH 
- * The result is written into the "cyclical" downsampleHk buffer 
+/**
+ * Downsample first axis in the slice present in buffer.downsampleH
+ * The result is written into the "cyclical" downsampleHk buffer
  * in the (L, H, K) axis order.
  */
 function downsampleHK(kernel: Data.Kernel, dimsX: number[], buffer: Data.DownsamplingBuffer) {
diff --git a/src/servers/volume/pack/format.ts b/src/servers/volume/pack/format.ts
new file mode 100644
index 0000000000000000000000000000000000000000..313d083e2033e8ea3e5ad96429bca5385b153fb0
--- /dev/null
+++ b/src/servers/volume/pack/format.ts
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as File from '../common/file'
+import { FileHandle } from 'mol-io/common/file-handle';
+import { Ccp4Provider } from './format/ccp4';
+import { TypedArrayBufferContext, TypedArrayValueArray, TypedArrayValueType, getElementByteSize, createTypedArrayBufferContext } from 'mol-io/common/typed-array';
+import { Dsn6Provider } from './format/dsn6';
+
+export interface Header {
+    name: string,
+    valueType: TypedArrayValueType,
+    grid: number[], // grid is converted to the axis order!!
+    axisOrder: number[],
+    extent: number[],
+    origin: number[],
+    spacegroupNumber: number,
+    cellSize: number[],
+    cellAngles: number[],
+    littleEndian: boolean,
+    dataOffset: number
+    originalHeader: unknown // TODO
+}
+
+/** Represents a circular buffer for 2 * blockSize layers */
+export interface SliceBuffer {
+    buffer: TypedArrayBufferContext,
+    maxBlockBytes: number
+    sliceCapacity: number,
+    slicesRead: number,
+
+    values: TypedArrayValueArray,
+    sliceCount: number,
+
+    /** Have all the input slice been read? */
+    isFinished: boolean
+}
+
+export interface Data {
+    header: Header,
+    file: FileHandle,
+    slices: SliceBuffer
+}
+
+export interface Provider {
+    readHeader: (name: string, file: FileHandle) => Promise<Header>,
+    readSlices: (data: Data) => Promise<void>
+}
+
+export interface Context {
+    data: Data,
+    provider: Provider
+}
+
+export function assignSliceBuffer(data: Data, blockSizeInMB: number) {
+    const { extent, valueType } = data.header;
+    const maxBlockBytes = blockSizeInMB * 1024 * 1024
+    const sliceSize = extent[0] * extent[1] * getElementByteSize(valueType);
+    const sliceCapacity = Math.max(1, Math.floor(Math.min(maxBlockBytes, sliceSize * extent[2]) / sliceSize));
+    const buffer = createTypedArrayBufferContext(sliceCapacity * extent[0] * extent[1], valueType);
+    data.slices = {
+        buffer,
+        maxBlockBytes,
+        sliceCapacity,
+        slicesRead: 0,
+        values: buffer.values,
+        sliceCount: 0,
+        isFinished: false
+    };
+}
+
+function compareProp(a: any, b: any) {
+    if (a instanceof Array && b instanceof Array) {
+        if (a.length !== b.length) return false;
+        for (let i = 0; i < a.length; i++) {
+            if (a[i] !== b[i]) return false;
+        }
+        return true;
+    }
+    return a === b;
+}
+
+export function compareHeaders(a: Header, b: Header) {
+    for (const p of ['grid', 'axisOrder', 'extent', 'origin', 'spacegroupNumber', 'cellSize', 'cellAngles', 'mode']) {
+        if (!compareProp((a as any)[p], (b as any)[p])) return false;
+    }
+    return true;
+}
+
+export type Type = 'ccp4' | 'dsn6'
+
+export function getProviderFromType(type: Type): Provider {
+    switch (type) {
+        case 'ccp4': return Ccp4Provider
+        case 'dsn6': return Dsn6Provider
+    }
+}
+
+export async function open(name: string, filename: string, type: Type): Promise<Context> {
+    const provider = getProviderFromType(type)
+    const descriptor = await File.openRead(filename);
+    const file = FileHandle.fromDescriptor(descriptor)
+    const header = await provider.readHeader(name, file);
+    const data = { header, file, slices: void 0 as any }
+    return { data, provider };
+}
\ No newline at end of file
diff --git a/src/servers/volume/pack/format/ccp4.ts b/src/servers/volume/pack/format/ccp4.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6269dff6571fc517cb9f881a6cd10889eba4a385
--- /dev/null
+++ b/src/servers/volume/pack/format/ccp4.ts
@@ -0,0 +1,57 @@
+/**
+ * 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 { FileHandle } from 'mol-io/common/file-handle';
+import { readCcp4Header, readCcp4Slices, getCcp4DataOffset, getCcp4ValueType } from 'mol-io/reader/ccp4/parser';
+import { Header, Provider, Data } from '../format';
+import { getCcp4Origin } from 'mol-model-formats/volume/ccp4';
+import { Ccp4Header } from 'mol-io/reader/ccp4/schema';
+
+async function readHeader(name: string, file: FileHandle) {
+    const { header: ccp4Header, littleEndian } = await readCcp4Header(file)
+
+    const header: Header = {
+        name,
+        valueType: getCcp4ValueType(ccp4Header),
+        grid: [ccp4Header.NX, ccp4Header.NY, ccp4Header.NZ],
+        axisOrder: [ccp4Header.MAPC, ccp4Header.MAPR, ccp4Header.MAPS].map(i => i - 1),
+        extent: [ccp4Header.NC, ccp4Header.NR, ccp4Header.NS],
+        origin: getCcp4Origin(ccp4Header),
+        spacegroupNumber: ccp4Header.ISPG,
+        cellSize: [ccp4Header.xLength, ccp4Header.yLength, ccp4Header.zLength],
+        cellAngles: [ccp4Header.alpha, ccp4Header.beta, ccp4Header.gamma],
+        littleEndian,
+        dataOffset: getCcp4DataOffset(ccp4Header),
+        originalHeader: ccp4Header
+    };
+    // "normalize" the grid axis order
+    header.grid = [header.grid[header.axisOrder[0]], header.grid[header.axisOrder[1]], header.grid[header.axisOrder[2]]];
+    return header;
+}
+
+export async function readSlices(data: Data) {
+    const { slices, header } = data;
+    if (slices.isFinished) {
+        return;
+    }
+
+    const { extent, originalHeader } = header;
+    const sliceSize = extent[0] * extent[1];
+    const sliceByteOffset = slices.buffer.elementByteSize * sliceSize * slices.slicesRead;
+    const sliceCount = Math.min(slices.sliceCapacity, extent[2] - slices.slicesRead);
+    const sliceByteCount = slices.buffer.elementByteSize * sliceCount * sliceSize;
+
+    await readCcp4Slices(originalHeader as Ccp4Header, slices.buffer, data.file, header.dataOffset + sliceByteOffset, sliceByteCount, header.littleEndian);
+    slices.slicesRead += sliceCount;
+    slices.sliceCount = sliceCount;
+
+    if (slices.slicesRead >= extent[2]) {
+        slices.isFinished = true;
+    }
+}
+
+export const Ccp4Provider: Provider = { readHeader, readSlices }
\ No newline at end of file
diff --git a/src/servers/volume/pack/format/dsn6.ts b/src/servers/volume/pack/format/dsn6.ts
new file mode 100644
index 0000000000000000000000000000000000000000..088a7bac5567e148e7358a20a849346d19aa6c04
--- /dev/null
+++ b/src/servers/volume/pack/format/dsn6.ts
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { FileHandle } from 'mol-io/common/file-handle';
+import { Header, Provider, Data } from '../format';
+import { readDsn6Header, dsn6HeaderSize, parseDsn6Values, getDsn6Counts } from 'mol-io/reader/dsn6/parser';
+import { TypedArrayValueType } from 'mol-io/common/typed-array';
+import { Dsn6Header } from 'mol-io/reader/dsn6/schema';
+
+async function readHeader(name: string, file: FileHandle) {
+    const { header: dsn6Header, littleEndian } = await readDsn6Header(file)
+
+    const header: Header = {
+        name,
+        valueType: TypedArrayValueType.Float32,
+        grid: [dsn6Header.xRate, dsn6Header.yRate, dsn6Header.zRate].reverse(),
+        axisOrder: [0, 1, 2].reverse(),
+        extent: [dsn6Header.xExtent, dsn6Header.yExtent, dsn6Header.zExtent].reverse(),
+        origin: [dsn6Header.xStart, dsn6Header.yStart, dsn6Header.zStart].reverse(),
+        spacegroupNumber: 1, // set as P 1, since it is not available in DSN6 files
+        cellSize: [dsn6Header.xlen, dsn6Header.ylen, dsn6Header.zlen],
+        cellAngles: [dsn6Header.alpha, dsn6Header.beta, dsn6Header.gamma],
+        littleEndian,
+        dataOffset: dsn6HeaderSize,
+        originalHeader: dsn6Header
+    };
+    return header;
+}
+
+export async function readSlices(data: Data) {
+    // TODO due to the dsn6 data layout we the read file into one big buffer
+    //      to avoid this, either change the sampling algoritm to work with this layout or
+    //      read the data into a collection of buffers that can be access like one big buffer
+    //      => for now not worth putting time in, for big files better use another file format
+
+    const { slices, header, file } = data;
+    if (slices.isFinished) {
+        return;
+    }
+
+    const { extent, dataOffset, originalHeader } = header;
+    const sliceCount = extent[2]
+
+    const { byteCount } = getDsn6Counts(originalHeader as Dsn6Header)
+    if (byteCount > slices.maxBlockBytes) {
+        throw new Error(`dsn6 file to large, can't read ${byteCount} bytes at once, increase block size or use another file format`)
+    }
+
+    const { buffer } = await file.readBuffer(dataOffset, byteCount)
+    if (!(slices.values instanceof Float32Array)) {
+        throw new Error(`dsn6 reader only supports Float32Array for output values`)
+    }
+    await parseDsn6Values(originalHeader as Dsn6Header, buffer, slices.values, header.littleEndian)
+
+    slices.slicesRead += sliceCount;
+    slices.sliceCount = sliceCount;
+
+    if (slices.slicesRead >= extent[2]) {
+        slices.isFinished = true;
+    }
+}
+
+export const Dsn6Provider: Provider = { readHeader, readSlices }
\ No newline at end of file
diff --git a/src/servers/volume/pack/main.ts b/src/servers/volume/pack/main.ts
index 116be4bb1f030f8546d0f5a8b48aa3566fb0d5a4..270682fbeb6f061f1752420102d9f1faa579238a 100644
--- a/src/servers/volume/pack/main.ts
+++ b/src/servers/volume/pack/main.ts
@@ -6,16 +6,16 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as CCP4 from './ccp4'
+import * as Format from './format'
 import * as File from '../common/file'
 import * as Data from './data-model'
 import * as Sampling from './sampling'
 import * as DataFormat from '../common/data-format'
-import * as fs from 'fs'
+import { FileHandle } from 'mol-io/common/file-handle';
 
-export default async function pack(input: { name: string, filename: string }[], blockSize: number, isPeriodic: boolean, outputFilename: string) {
+export default async function pack(input: { name: string, filename: string }[], blockSizeInMB: number, isPeriodic: boolean, outputFilename: string, format: Format.Type) {
     try {
-        await create(outputFilename, input, blockSize, isPeriodic);
+        await create(outputFilename, input, blockSizeInMB, isPeriodic, format);
     } catch (e) {
         console.error('[Error] ' + e);
     }
@@ -36,28 +36,28 @@ function updateAllocationProgress(progress: Data.Progress, progressDone: number)
 }
 
 /**
- * Pre allocate the disk space to be able to do "random" writes into the entire file. 
+ * Pre allocate the disk space to be able to do "random" writes into the entire file.
  */
 async function allocateFile(ctx: Data.Context) {
     const { totalByteSize, file } = ctx;
-    const buffer = new Buffer(Math.min(totalByteSize, 8 * 1024 * 1024));
+    const buffer = Buffer.alloc(Math.min(totalByteSize, 8 * 1024 * 1024));
     const progress: Data.Progress = { current: 0, max: Math.ceil(totalByteSize / buffer.byteLength) };
     let written = 0;
     while (written < totalByteSize) {
-        written += fs.writeSync(file, buffer, 0, Math.min(totalByteSize - written, buffer.byteLength));
+        written += file.writeBufferSync(written, buffer, Math.min(totalByteSize - written, buffer.byteLength));
         updateAllocationProgress(progress, 1);
     }
 }
 
-function determineBlockSize(data: CCP4.Data, blockSize: number) {
+function determineBlockSize(data: Format.Data, blockSizeInMB: number) {
     const { extent } = data.header;
     const maxLayerSize = 1024 * 1024 * 1024;
     const valueCount = extent[0] * extent[1];
-    if (valueCount * blockSize <= maxLayerSize) return blockSize;
+    if (valueCount * blockSizeInMB <= maxLayerSize) return blockSizeInMB;
 
-    while (blockSize > 0) {
-        blockSize -= 4;
-        if (valueCount * blockSize <= maxLayerSize) return blockSize;
+    while (blockSizeInMB > 0) {
+        blockSizeInMB -= 4;
+        if (valueCount * blockSizeInMB <= maxLayerSize) return blockSizeInMB;
     }
 
     throw new Error('Could not determine a valid block size.');
@@ -66,13 +66,13 @@ function determineBlockSize(data: CCP4.Data, blockSize: number) {
 async function writeHeader(ctx: Data.Context) {
     const header = DataFormat.encodeHeader(Data.createHeader(ctx));
     await File.writeInt(ctx.file, header.byteLength, 0);
-    await File.writeBuffer(ctx.file, 4, header);
+    await ctx.file.writeBuffer(4, header);
 }
 
-async function create(filename: string, sourceDensities: { name: string, filename: string }[], sourceBlockSize: number, isPeriodic: boolean) {
+async function create(filename: string, sourceDensities: { name: string, filename: string }[], sourceBlockSizeInMB: number, isPeriodic: boolean, format: Format.Type) {
     const startedTime = getTime();
 
-    if (sourceBlockSize % 4 !== 0 || sourceBlockSize < 4) {
+    if (sourceBlockSizeInMB % 4 !== 0 || sourceBlockSizeInMB < 4) {
         throw Error('Block size must be a positive number divisible by 4.');
     }
 
@@ -80,40 +80,42 @@ async function create(filename: string, sourceDensities: { name: string, filenam
         throw Error('Specify at least one source density.');
     }
 
-    process.stdout.write('Initializing... ');
-    const files: number[] = [];
+    process.stdout.write(`Initializing using ${format} format...`);
+    const files: FileHandle[] = [];
     try {
-        // Step 1a: Read the CCP4 headers
-        const channels: CCP4.Data[] = [];
-        for (const s of sourceDensities) channels.push(await CCP4.open(s.name, s.filename));
-        // Step 1b: Check if the CCP4 headers are compatible.
-        const isOk = channels.reduce((ok, s) => ok && CCP4.compareHeaders(channels[0].header, s.header), true);
+        // Step 1a: Read the Format headers
+        const channels: Format.Context[] = [];
+        for (const s of sourceDensities) {
+            channels.push(await Format.open(s.name, s.filename, format));
+        }
+        // Step 1b: Check if the Format headers are compatible.
+        const isOk = channels.reduce((ok, s) => ok && Format.compareHeaders(channels[0].data.header, s.data.header), true);
         if (!isOk) {
             throw new Error('Input file headers are not compatible (different grid, etc.).');
         }
-        const blockSize = determineBlockSize(channels[0], sourceBlockSize);
-        for (const ch of channels) CCP4.assignSliceBuffer(ch, blockSize);
+        const blockSizeInMB = determineBlockSize(channels[0].data, sourceBlockSizeInMB);
+        for (const ch of channels) Format.assignSliceBuffer(ch.data, blockSizeInMB);
 
         // Step 1c: Create data context.
-        const context = await Sampling.createContext(filename, channels, blockSize, isPeriodic);
-        for (const s of channels) files.push(s.file);
+        const context = await Sampling.createContext(filename, channels, blockSizeInMB, isPeriodic);
+        for (const s of channels) files.push(s.data.file);
         files.push(context.file);
         process.stdout.write('   done.\n');
 
-        console.log(`Block size: ${blockSize}`);
+        console.log(`Block size: ${blockSizeInMB}`);
 
-        // Step 2: Allocate disk space.        
+        // Step 2: Allocate disk space.
         process.stdout.write('Allocating...      0%');
         await allocateFile(context);
         process.stdout.write('\rAllocating...      done.\n');
 
-        // Step 3: Process and write the data 
+        // Step 3: Process and write the data
         process.stdout.write('Writing data...    0%');
         await Sampling.processData(context);
         process.stdout.write('\rWriting data...    done.\n');
 
         // Step 4: Write the header at the start of the file.
-        // The header is written last because the sigma/min/max values are computed 
+        // The header is written last because the sigma/min/max values are computed
         // during step 3.
         process.stdout.write('Writing header...  ');
         await writeHeader(context);
@@ -123,11 +125,11 @@ async function create(filename: string, sourceDensities: { name: string, filenam
         const time = getTime() - startedTime;
         console.log(`[Done] ${time.toFixed(0)}ms.`);
     } finally {
-        for (let f of files) File.close(f);
+        for (let f of files) f.close();
 
         // const ff = await File.openRead(filename);
         // const hh = await DataFormat.readHeader(ff);
         // File.close(ff);
         // console.log(hh.header);
     }
-} 
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/servers/volume/pack/sampling.ts b/src/servers/volume/pack/sampling.ts
index e193fb218bba78b243e4ebbcf4d837ec705bd552..299c1fb8cb802b6aeb76faf85bfffe861763802b 100644
--- a/src/servers/volume/pack/sampling.ts
+++ b/src/servers/volume/pack/sampling.ts
@@ -6,30 +6,33 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as CCP4 from './ccp4'
+import * as Format from './format'
 import * as Data from './data-model'
 import * as File from '../common/file'
 import * as Downsampling from './downsampling'
 import * as Writer from './writer'
 import * as DataFormat from '../common/data-format'
+import { FileHandle } from 'mol-io/common/file-handle';
+import { getElementByteSize, createTypedArray, TypedArrayValueType } from 'mol-io/common/typed-array';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
-export async function createContext(filename: string, channels: CCP4.Data[], blockSize: number, isPeriodic: boolean): Promise<Data.Context> {
-    const header = channels[0].header;
-    const samplingCounts = getSamplingCounts(channels[0].header.extent, blockSize);
-    const valueType = CCP4.getValueType(header);
-    const cubeBuffer = new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * DataFormat.getValueByteSize(valueType)));
+export async function createContext(filename: string, channels: Format.Context[], blockSize: number, isPeriodic: boolean): Promise<Data.Context> {
+    const { extent, valueType, grid, origin } = channels[0].data.header;
 
-    const litteEndianCubeBuffer = File.IsNativeEndianLittle
+    const samplingCounts = getSamplingCounts(extent, blockSize);
+    const cubeBuffer = Buffer.from(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * getElementByteSize(valueType)));
+
+    const litteEndianCubeBuffer = SimpleBuffer.IsNativeEndianLittle
         ? cubeBuffer
-        : new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * DataFormat.getValueByteSize(valueType)));
+        : Buffer.from(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * getElementByteSize(valueType)));
 
     // The data can be periodic iff the extent is the same as the grid and origin is 0.
-    if (header.grid.some((v, i) => v !== header.extent[i]) || header.origin.some(v => v !== 0)) {
+    if (grid.some((v, i) => v !== extent[i]) || origin.some(v => v !== 0)) {
         isPeriodic = false;
     }
 
     const ctx: Data.Context = {
-        file: await File.createFile(filename),
+        file: FileHandle.fromDescriptor(await File.createFile(filename)),
         isPeriodic,
         channels,
         valueType,
@@ -60,9 +63,9 @@ export async function createContext(filename: string, channels: CCP4.Data[], blo
 
 export async function processData(ctx: Data.Context) {
     const channel = ctx.channels[0];
-    while (!channel.slices.isFinished) {
+    while (!channel.data.slices.isFinished) {
         for (const src of ctx.channels) {
-            await CCP4.readSlices(src);
+            await src.provider.readSlices(src.data);
         }
         await processSlices(ctx);
     }
@@ -92,22 +95,22 @@ function getSamplingCounts(baseSampleCount: number[], blockSize: number) {
     }
 }
 
-function createBlockBuffer(sampleCount: number[], blockSize: number, valueType: DataFormat.ValueType, numChannels: number): Data.BlockBuffer {
+function createBlockBuffer(sampleCount: number[], blockSize: number, valueType: TypedArrayValueType, numChannels: number): Data.BlockBuffer {
     const values = [];
-    for (let i = 0; i < numChannels; i++) values[i] = DataFormat.createValueArray(valueType, sampleCount[0] * sampleCount[1] * blockSize);
+    for (let i = 0; i < numChannels; i++) values[i] = createTypedArray(valueType, sampleCount[0] * sampleCount[1] * blockSize);
     return {
         values,
-        buffers: values.map(xs => new Buffer(xs.buffer)),
+        buffers: values.map(xs => Buffer.from(xs.buffer)),
         slicesWritten: 0
     };
 }
 
-function createDownsamplingBuffer(valueType: DataFormat.ValueType, sourceSampleCount: number[], targetSampleCount: number[], numChannels: number): Data.DownsamplingBuffer[] {
+function createDownsamplingBuffer(valueType: TypedArrayValueType, sourceSampleCount: number[], targetSampleCount: number[], numChannels: number): Data.DownsamplingBuffer[] {
     const ret = [];
     for (let i = 0; i < numChannels; i++) {
         ret[ret.length] = {
-            downsampleH: DataFormat.createValueArray(valueType, sourceSampleCount[1] * targetSampleCount[0]),
-            downsampleHK: DataFormat.createValueArray(valueType, 5 * targetSampleCount[0] * targetSampleCount[1]),
+            downsampleH: createTypedArray(valueType, sourceSampleCount[1] * targetSampleCount[0]),
+            downsampleHK: createTypedArray(valueType, 5 * targetSampleCount[0] * targetSampleCount[1]),
             slicesWritten: 0,
             startSliceIndex: 0
         }
@@ -115,7 +118,7 @@ function createDownsamplingBuffer(valueType: DataFormat.ValueType, sourceSampleC
     return ret;
 }
 
-function createSampling(index: number, valueType: DataFormat.ValueType, numChannels: number, sampleCounts: number[][], blockSize: number): Data.Sampling {
+function createSampling(index: number, valueType: TypedArrayValueType, numChannels: number, sampleCounts: number[][], blockSize: number): Data.Sampling {
     const sampleCount = sampleCounts[index];
     const valuesInfo: Data.ValuesInfo[] = [];
     for (let i = 0; i < numChannels; i++) {
@@ -134,7 +137,7 @@ function createSampling(index: number, valueType: DataFormat.ValueType, numChann
         downsampling: index < sampleCounts.length - 1 ? createDownsamplingBuffer(valueType, sampleCount, sampleCounts[index + 1], numChannels) : void 0,
 
         byteOffset: 0,
-        byteSize: numChannels * sampleCount[0] * sampleCount[1] * sampleCount[2] * DataFormat.getValueByteSize(valueType),
+        byteSize: numChannels * sampleCount[0] * sampleCount[1] * sampleCount[2] * getElementByteSize(valueType),
         writeByteOffset: 0
     }
 }
@@ -148,7 +151,7 @@ function copyLayer(ctx: Data.Context, sliceIndex: number) {
     const targetOffset = blocks.slicesWritten * size;
 
     for (let channelIndex = 0; channelIndex < channels.length; channelIndex++) {
-        const src = channels[channelIndex].slices.values;
+        const src = channels[channelIndex].data.slices.values;
         const target = blocks.values[channelIndex];
         for (let i = 0; i < size; i++) {
             const v = src[srcOffset + i];
@@ -197,14 +200,14 @@ async function writeBlocks(ctx: Data.Context, isDataFinished: boolean) {
 
 async function processSlices(ctx: Data.Context) {
     const channel = ctx.channels[0];
-    const sliceCount = channel.slices.sliceCount;
+    const sliceCount = channel.data.slices.sliceCount;
     for (let i = 0; i < sliceCount; i++) {
         copyLayer(ctx, i);
         Downsampling.downsampleLayer(ctx);
 
         await writeBlocks(ctx, false);
 
-        const isDataFinished = i === sliceCount - 1 && channel.slices.isFinished;
+        const isDataFinished = i === sliceCount - 1 && channel.data.slices.isFinished;
         if (isDataFinished) {
             Downsampling.finalize(ctx);
             await writeBlocks(ctx, true);
diff --git a/src/servers/volume/pack/writer.ts b/src/servers/volume/pack/writer.ts
index 6cf357df36f23a3a36e0724d8709c97a511e4c6a..287feae38b140ded5127563e7b5868d59e283ab5 100644
--- a/src/servers/volume/pack/writer.ts
+++ b/src/servers/volume/pack/writer.ts
@@ -7,8 +7,8 @@
  */
 
 import * as Data from './data-model'
-import * as File from '../common/file'
-import * as DataFormat from '../common/data-format'
+import { getElementByteSize } from 'mol-io/common/typed-array';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 /** Converts a layer to blocks and writes them to the output file. */
 export async function writeBlockLayer(ctx: Data.Context, sampling: Data.Sampling) {
@@ -19,7 +19,7 @@ export async function writeBlockLayer(ctx: Data.Context, sampling: Data.Sampling
     for (let v = 0; v < nV; v++) {
         for (let u = 0; u < nU; u++) {
             const size = fillCubeBuffer(ctx, sampling, u, v);
-            await File.writeBuffer(ctx.file, startOffset + sampling.writeByteOffset, ctx.litteEndianCubeBuffer, size);
+            await ctx.file.writeBuffer(startOffset + sampling.writeByteOffset, ctx.litteEndianCubeBuffer, size);
             sampling.writeByteOffset += size;
             updateProgress(ctx.progress, 1);
         }
@@ -32,7 +32,7 @@ function fillCubeBuffer(ctx: Data.Context, sampling: Data.Sampling, u: number, v
     const { blockSize, cubeBuffer } = ctx;
     const { sampleCount } = sampling;
     const { buffers, slicesWritten } = sampling.blocks;
-    const elementSize = DataFormat.getValueByteSize(ctx.valueType);
+    const elementSize = getElementByteSize(ctx.valueType);
     const sizeH = sampleCount[0], sizeHK = sampleCount[0] * sampleCount[1];
     const offsetH = u * blockSize,
         offsetK = v * blockSize;
@@ -52,7 +52,7 @@ function fillCubeBuffer(ctx: Data.Context, sampling: Data.Sampling, u: number, v
         }
     }
     // flip the byte order if needed.
-    File.ensureLittleEndian(ctx.cubeBuffer, ctx.litteEndianCubeBuffer, writeOffset, elementSize, 0);
+    SimpleBuffer.ensureLittleEndian(ctx.cubeBuffer, ctx.litteEndianCubeBuffer, writeOffset, elementSize, 0);
     return writeOffset;
 }
 
diff --git a/src/servers/volume/server-config.ts b/src/servers/volume/server-config.ts
deleted file mode 100644
index 8a69aaed92aa4736214498e3a549556cba9c00c1..0000000000000000000000000000000000000000
--- a/src/servers/volume/server-config.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-
-const Config = {
-    limits: {
-        /**
-         * Maximum number of blocks that could be read in 1 query.
-         * This is somewhat tied to the maxOutputSizeInVoxelCountByPrecisionLevel
-         * in that the <maximum number of voxel> = maxRequestBlockCount * <block size>^3.
-         * The default block size is 96 which corresponds to 28,311,552 voxels with 32 max blocks.
-         */
-        maxRequestBlockCount: 32,
-
-        /**
-         * The maximum fractional volume of the query box (to prevent queries that are too big).
-         */
-        maxFractionalBoxVolume: 1024,
-
-        /**
-         * What is the (approximate) maximum desired size in voxel count by precision level
-         * Rule of thumb: <response gzipped size> \in [<voxel count> / 8, <voxel count> / 4];
-         *
-         * The maximum number of voxels is tied to maxRequestBlockCount.
-         */
-        maxOutputSizeInVoxelCountByPrecisionLevel: [
-            0.5 * 1024 * 1024, // ~ 80*80*80
-            1 * 1024 * 1024,
-            2 * 1024 * 1024,
-            4 * 1024 * 1024,
-            8 * 1024 * 1024,
-            16 * 1024 * 1024, // ~ 256*256*256
-            24 * 1024 * 1024
-        ]
-    },
-
-    /**
-     * Specify the prefix of the API, i.e.
-     * <host>/<apiPrefix>/<API queries>
-     */
-    apiPrefix: '/VolumeServer',
-
-    /**
-     * If not specified otherwise by the 'port' environment variable, use this port.
-     */
-    defaultPort: 1337,
-
-    /**
-     * Node (V8) sometimes exhibits GC related issues  that significantly slow down the execution
-     * (https://github.com/nodejs/node/issues/8670).
-     * 
-     * Therefore an option is provided that automatically shuts down the server.
-     * For this to work, the server must be run using a deamon (i.e. forever.js on Linux
-     * or IISnode on Windows) so that the server is automatically restarted when the shutdown happens.
-     */
-    shutdownParams: {
-        // 0 for off, server will shut down after this amount of minutes.
-        timeoutMinutes: 24 * 60 /* a day */,
-        // modifies the shutdown timer by +/- timeoutVarianceMinutes (to avoid multiple instances shutting at the same time)
-        timeoutVarianceMinutes: 60
-    },
-
-    /**
-     * Maps a request identifier to a filename.
-     * 
-     * @param source 
-     *   Source of the data.
-     * @param id
-     *   Id provided in the request. For xray, PDB id, for emd, EMDB id number. 
-     */
-    mapFile(source: string, id: string) {
-        switch (source.toLowerCase()) {
-            case 'x-ray': return `g:/test/mdb/xray-${id.toLowerCase()}.mdb`;
-            case 'emd': return `g:/test/mdb/${id.toLowerCase()}.mdb`;
-            default: return void 0;
-        }
-    }
-}
-
-export default Config;
\ No newline at end of file
diff --git a/src/servers/volume/server.ts b/src/servers/volume/server.ts
index 5d796f6287477133a8aef1894836214b94eeb40a..d8b6149a265082eca4cccc66e3b03dd8621e930e 100644
--- a/src/servers/volume/server.ts
+++ b/src/servers/volume/server.ts
@@ -1,9 +1,10 @@
 /**
- * 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.
  *
  * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import * as express from 'express'
@@ -11,19 +12,20 @@ import * as compression from 'compression'
 
 import init from './server/web-api'
 import VERSION from './server/version'
-import ServerConfig from './server-config'
 import { ConsoleLogger } from 'mol-util/console-logger'
 import { State } from './server/state'
+import { addServerArgs, addLimitsArgs, LimitsConfig, setConfig, ServerConfig } from './config';
+import * as argparse from 'argparse'
 
 function setupShutdown() {
-    if (ServerConfig.shutdownParams.timeoutVarianceMinutes > ServerConfig.shutdownParams.timeoutMinutes) {
+    if (ServerConfig.shutdownTimeoutVarianceMinutes > ServerConfig.shutdownTimeoutMinutes) {
         ConsoleLogger.log('Server', 'Shutdown timeout variance is greater than the timer itself, ignoring.');
     } else {
         let tVar = 0;
-        if (ServerConfig.shutdownParams.timeoutVarianceMinutes > 0) {
-            tVar = 2 * (Math.random() - 0.5) * ServerConfig.shutdownParams.timeoutVarianceMinutes;
+        if (ServerConfig.shutdownTimeoutVarianceMinutes > 0) {
+            tVar = 2 * (Math.random() - 0.5) * ServerConfig.shutdownTimeoutVarianceMinutes;
         }
-        let tMs = (ServerConfig.shutdownParams.timeoutMinutes + tVar) * 60 * 1000;
+        let tMs = (ServerConfig.shutdownTimeoutMinutes + tVar) * 60 * 1000;
 
         console.log(`----------------------------------------------------------------------------`);
         console.log(`  The server will shut down in ${ConsoleLogger.formatTime(tMs)} to prevent slow performance.`);
@@ -42,20 +44,29 @@ function setupShutdown() {
     }
 }
 
+const parser = new argparse.ArgumentParser({
+    addHelp: true,
+    description: `VolumeServer ${VERSION}, (c) 2018-2019, Mol* contributors`
+});
+addServerArgs(parser)
+addLimitsArgs(parser)
 
-let port = process.env.port || ServerConfig.defaultPort;
+const config: ServerConfig & LimitsConfig = parser.parseArgs()
+setConfig(config) // sets the config for global use
 
-let app = express();
+const port = process.env.port || ServerConfig.defaultPort;
+
+const app = express();
 app.use(compression({ level: 6, memLevel: 9, chunkSize: 16 * 16384, filter: () => true }));
 init(app);
 
 app.listen(port);
 
-console.log(`VolumeServer ${VERSION}, (c) 2016 - now, David Sehnal`);
+console.log(`VolumeServer ${VERSION}, (c) 2018-2019, Mol* contributors`);
 console.log(``);
 console.log(`The server is running on port ${port}.`);
 console.log(``);
 
-if (ServerConfig.shutdownParams && ServerConfig.shutdownParams.timeoutMinutes > 0) {
+if (config.shutdownTimeoutMinutes > 0) {
     setupShutdown();
 }
\ No newline at end of file
diff --git a/src/servers/volume/server/api.ts b/src/servers/volume/server/api.ts
index c1046a4875498e357e62f37450109b264161a797..e3a9c247ab5808dc5a10e5e0a88b7302f0777154 100644
--- a/src/servers/volume/server/api.ts
+++ b/src/servers/volume/server/api.ts
@@ -11,40 +11,46 @@ import execute from './query/execute'
 import * as Data from './query/data-model'
 import { ConsoleLogger } from 'mol-util/console-logger'
 import * as DataFormat from '../common/data-format'
-import ServerConfig from '../server-config'
+import { FileHandle } from 'mol-io/common/file-handle';
+import { LimitsConfig } from '../config';
 
 export function getOutputFilename(source: string, id: string, { asBinary, box, detail, forcedSamplingLevel }: Data.QueryParams) {
     function n(s: string) { return (s || '').replace(/[ \n\t]/g, '').toLowerCase() }
     function r(v: number) { return Math.round(10 * v) / 10; }
     const det = forcedSamplingLevel !== void 0
         ? `l${forcedSamplingLevel}`
-        : `d${Math.min(Math.max(0, detail | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1)}`;
+        : `d${Math.min(Math.max(0, detail | 0), LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1)}`;
     const boxInfo = box.kind === 'Cell'
         ? 'cell'
         : `${box.kind === 'Cartesian' ? 'cartn' : 'frac'}_${r(box.a[0])}_${r(box.a[1])}_${r(box.a[2])}_${r(box.b[0])}_${r(box.b[1])}_${r(box.b[2])}`;
     return `${n(source)}_${n(id)}-${boxInfo}_${det}.${asBinary ? 'bcif' : 'cif'}`;
 }
 
+export interface ExtendedHeader extends DataFormat.Header {
+    availablePrecisions: { precision: number, maxVoxels: number }[]
+    isAvailable: boolean
+}
+
 /** Reads the header and includes information about available detail levels */
-export async function getHeaderJson(filename: string | undefined, sourceId: string) {
+export async function getExtendedHeaderJson(filename: string | undefined, sourceId: string) {
     ConsoleLogger.log('Header', sourceId);
     try {
         if (!filename || !File.exists(filename)) {
             ConsoleLogger.error(`Header ${sourceId}`, 'File not found.');
             return void 0;
         }
-        const header = { ...await readHeader(filename, sourceId) } as DataFormat.Header;
-        const { sampleCount } = header!.sampling[0];
+        const header: Partial<ExtendedHeader> = { ...await readHeader(filename, sourceId) };
+        const { sampleCount } = header.sampling![0];
         const maxVoxelCount = sampleCount[0] * sampleCount[1] * sampleCount[2];
-        const precisions = ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel
+        const precisions = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel
             .map((maxVoxels, precision) => ({ precision, maxVoxels }));
         const availablePrecisions = [];
         for (const p of precisions) {
             availablePrecisions.push(p);
             if (p.maxVoxels > maxVoxelCount) break;
         }
-        (header as any).availablePrecisions = availablePrecisions;
-        (header as any).isAvailable = true;
+        header.availablePrecisions = availablePrecisions;
+        header.isAvailable = true;
         return JSON.stringify(header, null, 2);
     } catch (e) {
         ConsoleLogger.error(`Header ${sourceId}`, e);
@@ -57,16 +63,16 @@ export async function queryBox(params: Data.QueryParams, outputProvider: () => D
 }
 
 async function readHeader(filename: string | undefined, sourceId: string) {
-    let file: number | undefined = void 0;
+    let file: FileHandle | undefined;
     try {
         if (!filename) return void 0;
-        file = await File.openRead(filename);
+        file = FileHandle.fromDescriptor(await File.openRead(filename));
         const header = await DataFormat.readHeader(file);
         return header.header;
     } catch (e) {
         ConsoleLogger.error(`Info ${sourceId}`, e);
         return void 0;
     } finally {
-        File.close(file);
+        if (file) file.close();
     }
 }
\ No newline at end of file
diff --git a/src/servers/volume/server/documentation.ts b/src/servers/volume/server/documentation.ts
deleted file mode 100644
index 8fa43ad817af85f12cbb1c7aa44b28f4a8c034b3..0000000000000000000000000000000000000000
--- a/src/servers/volume/server/documentation.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-/**
- * Copyright (c) 2018 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>
- */
-
-import VERSION from './version'
-import ServerConfig from '../server-config'
-
-function detail(i: number) {
-     return `<span class='id'>${i}</span><small> (${Math.round(100 * ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)</small>`;
-}
-const detailMax = ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
-const dataSource = `Specifies the data source (determined by the experiment method). Currently, <span class='id'>x-ray</span> and <span class='id'>em</span> sources are supported.`;
-const entryId = `Id of the entry. For <span class='id'>x-ray</span>, use PDB ID (i.e. <span class='id'>1cbs</span>) and for <span class='id'>em</span> use EMDB id (i.e. <span class='id'>emd-8116</span>).`;
-
-export default `
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-<meta charset="utf-8" />
-<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />
-<title>VolumeServer (${VERSION})</title>
-<style>
-html { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; }
-body { margin: 0; font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; font-weight: 300; color: #333; line-height: 1.42857143; font-size: 14px }
-.container { padding: 0 15px; max-width: 970px; margin: 0 auto; }
-small { font-size: 80% }
-h2, h4 { font-weight: 500; line-height: 1.1; }
-h2 { color: black; font-size: 24px; }
-h4 { font-size: 18px; margin: 20px 0 10px 0 }
-h2 small { color: #777; font-weight: 300 }
-hr { box-sizing: content-box; height: 0; overflow: visible; }
-a { background-color: transparent; -webkit-text-decoration-skip: objects; text-decoration: none }
-a:active, a:hover { outline-width: 0; }
-a:focus, a:hover { text-decoration: underline; color: #23527c }
-.list-unstyled { padding: 0; list-style: none; margin: 0 0 10px 0 }
-.cs-docs-query-wrap { padding: 24px 0; border-bottom: 1px solid #eee }
-.cs-docs-query-wrap > h2 { margin: 0; color: black; }
-.cs-docs-query-wrap > h2 > span { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; font-size: 90% }
-.cs-docs-param-name, .cs-docs-template-link { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace }
-table {margin: 0; padding: 0; }
-table th { font-weight: bold; border-bottom: none; text-align: left; padding: 6px 12px }
-td { padding: 6px 12px }
-td:not(:last-child), th:not(:last-child) { border-right: 1px dotted #ccc }
-tr:nth-child(even) { background: #f9f9f9 }
-span.id  { color: #DE4D4E; font-family: Menlo,Monaco,Consolas,"Courier New",monospace; }
-</style>
-</head>
-<body>
-<div class="container">
-<div style='text-align: center; margin-top: 24px;'><span style='font-weight: bold; font-size: 16pt'>VolumeServer</span> <span>${VERSION}</span></div>
-
-<div style='text-align: justify; padding: 24px 0; border-bottom: 1px solid #eee'>
-  <p>
-    <b>VolumeServer</b> is a service for accessing subsets of volumetric density data. It automatically downsamples the data
-    depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the
-    largest data sets.
-  </p>
-  <p>
-    It uses the text based <a href='https://en.wikipedia.org/wiki/Crystallographic_Information_File'>CIF</a> and binary
-    <a href='https://github.com/dsehnal/BinaryCIF' style='font-weight: bold'>BinaryCIF</a>
-    formats to deliver the data to the client.
-    The server support is integrated into the <a href='https://github.com/dsehnal/LiteMol' style='font-weight: bold'>LiteMol Viewer</a>.
-  </p>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Data Header / Check Availability <span>/&lt;source&gt;/&lt;id&gt;</span><br>
-  <small>Returns a JSON response specifying if data is available and the maximum region that can be queried.</small></h2>
-  <div id="coordserver-documentation-ambientResidues-body" style="margin: 24px 24px 0 24px">
-    <h4>Examples</h4>
-    <a href="/VolumeServer/x-ray/1cbs" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs</a><br>
-    <a href="/VolumeServer/em/emd-8116" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116</a>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Box <span>/&lt;source&gt;/&lt;id&gt;/box/&lt;a,b,c&gt;/&lt;u,v,w&gt;?&lt;optional parameters&gt;</span><br>
-  <small>Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.</small></h2>
-  <div style="margin: 24px 24px 0 24px">
-    <h4>Examples</h4>
-    <a href="/VolumeServer/em/emd-8003/box/-2,7,10/4,10,15.5?encoding=cif&space=cartesian" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8003/box/-2,7,10/4,10,15.5?excoding=cif&space=cartesian</a><br>
-    <a href="/VolumeServer/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional" class="cs-docs-template-link" target="_blank" rel="nofollow">/x-ray/1cbs/box/0.1,0.1,0.1/0.23,0.31,0.18?space=fractional</a>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">a,b,c</td>
-    <td>Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">u,v,w</td>
-    <td>Top right corner of the query region in Cartesian or fractional coordinates (determined by the <span class='id'>&amp;space</span> query parameter).</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">encoding</td>
-    <td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">space</td>
-    <td>Determines the coordinate space the query is in. Can be <span class='id'>cartesian</span> or <span class='id'>fractional</span>. An optional argument, default values is <span class='id'>cartesian</span>.</td>
-    </tr>
-    <tr>
-      <td class="cs-docs-param-name">detail</td>
-      <td>
-        Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
-        Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
-      </td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-<div class="cs-docs-query-wrap">
-  <h2>Cell <span>/&lt;source&gt;/&lt;id&gt;/cell?&lt;optional parameters&gt;</span><br>
-  <small>Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.</small></h2>
-  <div style="margin: 24px 24px 0 24px">
-    <h4>Example</h4>
-    <a href="/VolumeServer/em/emd-8116/cell?detail=1" class="cs-docs-template-link" target="_blank" rel="nofollow">/em/emd-8116/cell?detail=1</a><br>
-    <h4>Parameters</h4>
-    <table cellpadding="0" cellspacing="0" style='width: 100%'>
-    <tbody><tr><th style='width: 80px'>Name</th><th>Description</th></tr>
-    <tr>
-    <td class="cs-docs-param-name">source</td>
-    <td>${dataSource}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">id</td>
-    <td>${entryId}</td>
-    </tr>
-    <tr>
-    <td class="cs-docs-param-name">encoding</td>
-    <td>Determines if text based <span class='id'>CIF</span> or binary <span class='id'>BinaryCIF</span> encoding is used. An optional argument, default is <span class='id'>BinaryCIF</span> encoding.</td>
-    </tr>
-    <tr>
-      <td class="cs-docs-param-name">detail</td>
-      <td>
-        Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}.
-        Default value is <span class='id'>0</span>. Note: different detail levels might lead to the same result.
-      </td>
-    </tr>
-    </tbody></table>
-  </div>
-</div>
-
-
-<div style="color: #999;font-size:smaller;margin: 20px 0; text-align: right">&copy; 2016 &ndash; now, David Sehnal | Node ${process.version}</div>
-
-</body>
-</html>
-`;
\ No newline at end of file
diff --git a/src/servers/volume/server/local-api.ts b/src/servers/volume/server/local-api.ts
index 8a4a514db1cb3cb4234c4b03e3766e5096d6779b..de87df318ec3e7cf457a92aa932598e2bd7b666b 100644
--- a/src/servers/volume/server/local-api.ts
+++ b/src/servers/volume/server/local-api.ts
@@ -28,7 +28,7 @@ export interface JobEntry {
     params: {
         /** Determines the detail level as specified in server-config */
         detail?: number,
-        /** 
+        /**
          * Determines the sampling level:
          * 1: Original data
          * 2: Downsampled by factor 1/2
@@ -121,7 +121,7 @@ function wrapFile(fn: string) {
         },
         writeBinary(this: any, data: Uint8Array) {
             this.open();
-            fs.writeSync(this.file, new Buffer(data));
+            fs.writeSync(this.file, Buffer.from(data));
             return true;
         },
         writeString(this: any, data: string) {
diff --git a/src/servers/volume/server/query/compose.ts b/src/servers/volume/server/query/compose.ts
index af6f8ce8255d63f6d393eb96131a636683daa934..29f546c11439fae5931cbbf2fa4e61682c16080c 100644
--- a/src/servers/volume/server/query/compose.ts
+++ b/src/servers/volume/server/query/compose.ts
@@ -6,11 +6,10 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as DataFormat from '../../common/data-format'
 import * as Data from './data-model'
 import * as Box from '../algebra/box'
 import * as Coords from '../algebra/coordinate'
-import * as File from '../../common/file'
+import { createTypedArrayBufferContext, getElementByteSize, readTypedArray } from 'mol-io/common/typed-array';
 
 export default async function compose(query: Data.QueryContext.Data) {
     for (const block of query.samplingInfo.blocks) {
@@ -19,19 +18,21 @@ export default async function compose(query: Data.QueryContext.Data) {
 }
 
 async function readBlock(query: Data.QueryContext.Data, coord: Coords.Grid<'Block'>, blockBox: Box.Fractional): Promise<Data.BlockData> {
+    const { valueType, blockSize } = query.data.header;
+    const elementByteSize = getElementByteSize(valueType)
     const numChannels = query.data.header.channels.length;
     const blockSampleCount = Box.dimensions(Box.fractionalToGrid(blockBox, query.samplingInfo.sampling.dataDomain));
     const size = numChannels * blockSampleCount[0] * blockSampleCount[1] * blockSampleCount[2];
-    const { valueType, blockSize } = query.data.header;
+    const byteSize = elementByteSize * size
     const dataSampleCount = query.data.header.sampling[query.samplingInfo.sampling.index].sampleCount;
-    const buffer = File.createTypedArrayBufferContext(size, valueType);
+    const buffer = createTypedArrayBufferContext(size, valueType);
     const byteOffset = query.samplingInfo.sampling.byteOffset
-        + DataFormat.getValueByteSize(valueType) * numChannels * blockSize
+        + elementByteSize * numChannels * blockSize
         * (blockSampleCount[1] * blockSampleCount[2] * coord[0]
             + dataSampleCount[0] * blockSampleCount[2] * coord[1]
             + dataSampleCount[0] * dataSampleCount[1] * coord[2]);
 
-    const values = await File.readTypedArray(buffer, query.data.file, byteOffset, size, 0);
+    const values = await readTypedArray(buffer, query.data.file, byteOffset, byteSize, 0);
     return {
         sampleCount: blockSampleCount,
         values
diff --git a/src/servers/volume/server/query/data-model.ts b/src/servers/volume/server/query/data-model.ts
index d20c192ce4b1c319fac66eedc57858ab319c9e7d..1536a797931ae8bf0afa47f32f29bf72c86eace9 100644
--- a/src/servers/volume/server/query/data-model.ts
+++ b/src/servers/volume/server/query/data-model.ts
@@ -11,6 +11,8 @@ import * as Coords from '../algebra/coordinate'
 import * as Box from '../algebra/box'
 import Writer from 'mol-io/writer/writer'
 import { SpacegroupCell } from 'mol-math/geometry';
+import { FileHandle } from 'mol-io/common/file-handle';
+import { TypedArrayValueArray } from 'mol-io/common/typed-array';
 
 //////////////////////////////////////
 // DATA
@@ -25,7 +27,7 @@ export interface Sampling {
 }
 
 export interface DataContext {
-    file: number,
+    file: FileHandle,
     header: DataFormat.Header,
     spacegroup: SpacegroupCell,
     dataBox: Box.Fractional,
@@ -34,7 +36,7 @@ export interface DataContext {
 
 export interface BlockData {
     sampleCount: number[],
-    values: DataFormat.ValueArray
+    values: TypedArrayValueArray
 }
 
 //////////////////////////////////////
@@ -74,5 +76,5 @@ export namespace QueryContext {
     type Base = { guid: string, params: QueryParams }
     export type Error = { kind: 'Error', message: string } & Base
     export type Empty = { kind: 'Empty', data: DataContext } & Base
-    export type Data = { kind: 'Data', data: DataContext, samplingInfo: QuerySamplingInfo, values: DataFormat.ValueArray[] } & Base
+    export type Data = { kind: 'Data', data: DataContext, samplingInfo: QuerySamplingInfo, values: TypedArrayValueArray[] } & Base
 }
\ No newline at end of file
diff --git a/src/servers/volume/server/query/encode.ts b/src/servers/volume/server/query/encode.ts
index f456c9edf3951c3c11c2e6d14497cecf1892cf09..008159dfdfc8995b3a1c330ead00176a96e9b524 100644
--- a/src/servers/volume/server/query/encode.ts
+++ b/src/servers/volume/server/query/encode.ts
@@ -13,6 +13,7 @@ import VERSION from '../version'
 import * as DataFormat from '../../common/data-format'
 import { Column } from 'mol-data/db';
 import { ArrayEncoding, ArrayEncoder } from 'mol-io/common/binary-cif';
+import { TypedArrayValueType, TypedArrayValueArray } from 'mol-io/common/typed-array';
 
 export default function encode(query: Data.QueryContext, output: Data.QueryOutputStream) {
     let w = CifWriter.createEncoder({ binary: query.params.asBinary, encoderName: `VolumeServer ${VERSION}` });
@@ -106,7 +107,7 @@ const _volume_data_3d_info: CifWriter.Category<ResultContext> = {
     }
 };
 
-function _volume_data_3d_number(i: number, ctx: DataFormat.ValueArray): number {
+function _volume_data_3d_number(i: number, ctx: TypedArrayValueArray): number {
     return ctx[i];
 }
 
@@ -118,7 +119,7 @@ const _volume_data_3d: CifWriter.Category<ResultContext> = {
         const E = ArrayEncoding;
         let encoder: ArrayEncoder;
         let typedArray: any;
-        if (ctx.query.data.header.valueType === DataFormat.ValueType.Float32 || ctx.query.data.header.valueType === DataFormat.ValueType.Int16) {
+        if (ctx.query.data.header.valueType === TypedArrayValueType.Float32 || ctx.query.data.header.valueType === TypedArrayValueType.Int16) {
             let min: number, max: number;
             min = data[0], max = data[0];
             for (let i = 0, n = data.length; i < n; i++) {
diff --git a/src/servers/volume/server/query/execute.ts b/src/servers/volume/server/query/execute.ts
index c5d4272dfdbb87975ec80759d5abab86e4cdf6cf..9314cb5217c2aec09ba3bbd0f81f03bec912f5a4 100644
--- a/src/servers/volume/server/query/execute.ts
+++ b/src/servers/volume/server/query/execute.ts
@@ -13,7 +13,6 @@ import * as Coords from '../algebra/coordinate'
 import * as Box from '../algebra/box'
 import { ConsoleLogger } from 'mol-util/console-logger'
 import { State } from '../state'
-import ServerConfig from '../../server-config'
 
 import identify from './identify'
 import compose from './compose'
@@ -21,25 +20,28 @@ import encode from './encode'
 import { SpacegroupCell } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UUID } from 'mol-util';
+import { FileHandle } from 'mol-io/common/file-handle';
+import { createTypedArray, TypedArrayValueType } from 'mol-io/common/typed-array';
+import { LimitsConfig } from 'servers/volume/config';
 
 export default async function execute(params: Data.QueryParams, outputProvider: () => Data.QueryOutputStream) {
     const start = getTime();
     State.pendingQueries++;
 
     const guid = UUID.create22() as any as string;
-    params.detail = Math.min(Math.max(0, params.detail | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1);
+    params.detail = Math.min(Math.max(0, params.detail | 0), LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1);
     ConsoleLogger.logId(guid, 'Info', `id=${params.sourceId},encoding=${params.asBinary ? 'binary' : 'text'},detail=${params.detail},${queryBoxToString(params.box)}`);
 
-    let sourceFile: number | undefined = void 0;
+    let sourceFile: FileHandle | undefined;
     try {
-        sourceFile = await File.openRead(params.sourceFilename);
+        sourceFile = FileHandle.fromDescriptor(await File.openRead(params.sourceFilename));
         await _execute(sourceFile, params, guid, outputProvider);
         return true;
     } catch (e) {
         ConsoleLogger.errorId(guid, e);
         return false;
     } finally {
-        File.close(sourceFile);
+        if (sourceFile) sourceFile.close();
         ConsoleLogger.logId(guid, 'Time', `${Math.round(getTime() - start)}ms`);
         State.pendingQueries--;
     }
@@ -80,7 +82,7 @@ function createSampling(header: DataFormat.Header, index: number, dataOffset: nu
     }
 }
 
-async function createDataContext(file: number): Promise<Data.DataContext> {
+async function createDataContext(file: FileHandle): Promise<Data.DataContext> {
     const { header, dataOffset } = await DataFormat.readHeader(file);
 
     const origin = Coords.fractional(header.origin[0], header.origin[1], header.origin[2]);
@@ -112,7 +114,7 @@ function pickSampling(data: Data.DataContext, queryBox: Box.Fractional, forcedLe
         return createQuerySampling(data, data.sampling[Math.min(data.sampling.length, forcedLevel) - 1], queryBox);
     }
 
-    const sizeLimit = ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel[precision] || (2 * 1024 * 1024);
+    const sizeLimit = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[precision] || (2 * 1024 * 1024);
 
     for (const s of data.sampling) {
         const gridBox = Box.fractionalToGrid(queryBox, s.dataDomain);
@@ -120,7 +122,7 @@ function pickSampling(data: Data.DataContext, queryBox: Box.Fractional, forcedLe
 
         if (approxSize <= sizeLimit) {
             const sampling = createQuerySampling(data, s, queryBox);
-            if (sampling.blocks.length <= ServerConfig.limits.maxRequestBlockCount) {
+            if (sampling.blocks.length <= LimitsConfig.maxRequestBlockCount) {
                 return sampling;
             }
         }
@@ -141,10 +143,10 @@ function getQueryBox(data: Data.DataContext, queryBox: Data.QueryParamsBox) {
     }
 }
 
-function allocateValues(domain: Coords.GridDomain<'Query'>, numChannels: number, valueType: DataFormat.ValueType) {
+function allocateValues(domain: Coords.GridDomain<'Query'>, numChannels: number, valueType: TypedArrayValueType) {
     const values = [];
     for (let i = 0; i < numChannels; i++) {
-        values[values.length] = DataFormat.createValueArray(valueType, domain.sampleVolume);
+        values[values.length] = createTypedArray(valueType, domain.sampleVolume);
     }
     return values;
 }
@@ -166,7 +168,7 @@ function createQueryContext(data: Data.DataContext, params: Data.QueryParams, gu
         throw `The query box is not defined.`;
     }
 
-    if (dimensions[0] * dimensions[1] * dimensions[2] > ServerConfig.limits.maxFractionalBoxVolume) {
+    if (dimensions[0] * dimensions[1] * dimensions[2] > LimitsConfig.maxFractionalBoxVolume) {
         throw `The query box volume is too big.`;
     }
 
@@ -185,7 +187,7 @@ function createQueryContext(data: Data.DataContext, params: Data.QueryParams, gu
 }
 
 
-async function _execute(file: number, params: Data.QueryParams, guid: string, outputProvider: () => Data.QueryOutputStream) {
+async function _execute(file: FileHandle, params: Data.QueryParams, guid: string, outputProvider: () => Data.QueryOutputStream) {
     let output: any = void 0;
     try {
         // Step 1a: Create data context
diff --git a/src/servers/volume/server/web-api.ts b/src/servers/volume/server/web-api.ts
index 5040ba702e85fd1c6396f7c3a016fc8295d555eb..9630f314b9f63a0e51293ef0b40b01a879a7dda8 100644
--- a/src/servers/volume/server/web-api.ts
+++ b/src/servers/volume/server/web-api.ts
@@ -1,25 +1,28 @@
 /**
- * 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.
  *
  * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import * as express from 'express'
 
 import * as Api from './api'
-
 import * as Data from './query/data-model'
 import * as Coords from './algebra/coordinate'
-import Docs from './documentation'
-import ServerConfig from '../server-config'
 import { ConsoleLogger } from 'mol-util/console-logger'
 import { State } from './state'
+import { LimitsConfig, ServerConfig } from '../config';
+import { interpolate } from 'mol-util/string';
+import { getSchema, shortcutIconLink } from './web-schema';
+import { swaggerUiIndexHandler, swaggerUiAssetsHandler } from 'servers/common/swagger-ui';
 
 export default function init(app: express.Express) {
+    app.locals.mapFile = getMapFileFn()
     function makePath(p: string) {
-        return ServerConfig.apiPrefix + '/' + p;
+        return `${ServerConfig.apiPrefix}/${p}`;
     }
 
     // Header
@@ -29,18 +32,40 @@ export default function init(app: express.Express) {
     // Cell /:src/:id/cell/?text=0|1&space=cartesian|fractional
     app.get(makePath(':source/:id/cell/?'), (req, res) => queryBox(req, res, getQueryParams(req, true)));
 
-    app.get('*', (req, res) => {
-        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
-        res.end(Docs);
+    app.get(makePath('openapi.json'), (req, res) => {
+        res.writeHead(200, {
+            'Content-Type': 'application/json; charset=utf-8',
+            'Access-Control-Allow-Origin': '*',
+            'Access-Control-Allow-Headers': 'X-Requested-With'
+        });
+        res.end(JSON.stringify(getSchema()));
     });
+
+    app.use(makePath(''), swaggerUiAssetsHandler());
+    app.get(makePath(''), swaggerUiIndexHandler({
+        openapiJsonUrl: makePath('openapi.json'),
+        apiPrefix: ServerConfig.apiPrefix,
+        title: 'VolumeServer API',
+        shortcutIconLink
+    }));
 }
 
-function mapFile(type: string, id: string) {
-    return ServerConfig.mapFile(type || '', id || '');
+function getMapFileFn() {
+    const map = new Function('type', 'id', 'interpolate', [
+        'id = id.toLowerCase()',
+        'switch (type.toLowerCase()) {',
+            ...ServerConfig.idMap.map(mapping => {
+                const [type, path] = mapping
+                return `    case '${type}': return interpolate('${path}', { id });`
+            }),
+        '    default: return void 0;',
+        '}'
+    ].join('\n'))
+    return (type: string, id: string) => map(type, id, interpolate)
 }
 
 function wrapResponse(fn: string, res: express.Response) {
-    const w = {
+    return {
         do404(this: any) {
             if (!this.headerWritten) {
                 res.writeHead(404);
@@ -60,7 +85,7 @@ function wrapResponse(fn: string, res: express.Response) {
         },
         writeBinary(this: any, data: Uint8Array) {
             if (!this.headerWritten) this.writeHeader(true);
-            return res.write(new Buffer(data.buffer));
+            return res.write(Buffer.from(data.buffer));
         },
         writeString(this: any, data: string) {
             if (!this.headerWritten) this.writeHeader(false);
@@ -74,13 +99,11 @@ function wrapResponse(fn: string, res: express.Response) {
         ended: false,
         headerWritten: false
     };
-
-    return w;
 }
 
 function getSourceInfo(req: express.Request) {
     return {
-        filename: mapFile(req.params.source, req.params.id),
+        filename: req.app.locals.mapFile(req.params.source, req.params.id),
         id: `${req.params.source}/${req.params.id}`
     };
 }
@@ -104,7 +127,7 @@ async function getHeader(req: express.Request, res: express.Response) {
 
     try {
         const { filename, id } = getSourceInfo(req);
-        const header = await Api.getHeaderJson(filename, id);
+        const header = await Api.getExtendedHeaderJson(filename, id);
         if (!header) {
             res.writeHead(404);
             return;
@@ -130,7 +153,7 @@ function getQueryParams(req: express.Request, isCell: boolean): Data.QueryParams
     const a = [+req.params.a1, +req.params.a2, +req.params.a3];
     const b = [+req.params.b1, +req.params.b2, +req.params.b3];
 
-    const detail = Math.min(Math.max(0, (+req.query.detail) | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1)
+    const detail = Math.min(Math.max(0, (+req.query.detail) | 0), LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1)
     const isCartesian = (req.query.space || '').toLowerCase() !== 'fractional';
 
     const box: Data.QueryParamsBox = isCell
@@ -140,7 +163,7 @@ function getQueryParams(req: express.Request, isCell: boolean): Data.QueryParams
             : { kind: 'Fractional', a: Coords.fractional(a[0], a[1], a[2]), b: Coords.fractional(b[0], b[1], b[2]) });
 
     const asBinary = (req.query.encoding || '').toLowerCase() !== 'cif';
-    const sourceFilename = mapFile(req.params.source, req.params.id)!;
+    const sourceFilename = req.app.locals.mapFile(req.params.source, req.params.id)!;
 
     return {
         sourceFilename,
diff --git a/src/servers/volume/server/web-schema.ts b/src/servers/volume/server/web-schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05c743af26cb9dde7e9bd9c197e0c70c284f916d
--- /dev/null
+++ b/src/servers/volume/server/web-schema.ts
@@ -0,0 +1,261 @@
+/**
+ * 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 VERSION from './version'
+import { LimitsConfig, ServerConfig } from '../config';
+
+export function getSchema() {
+    function detail(i: number) {
+       return `${i} (${Math.round(100 * LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel[i] / 1000 / 1000) / 100 }M voxels)`;
+    }
+    const detailMax = LimitsConfig.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1;
+    const sources = ServerConfig.idMap.map(m => m[0])
+
+    return {
+        openapi: '3.0.0',
+        info: {
+            version: VERSION,
+            title: 'Volume Server',
+            description: 'The VolumeServer is a service for accessing subsets of volumetric data. It automatically downsamples the data depending on the volume of the requested region to reduce the bandwidth requirements and provide near-instant access to even the largest data sets.',
+        },
+        tags: [
+            {
+                name: 'General',
+            }
+        ],
+        paths: {
+            [`${ServerConfig.apiPrefix}/{source}/{id}/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns a JSON response specifying if data is available and the maximum region that can be queried.',
+                    operationId: 'getInfo',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume availability and info',
+                            content: {
+                                'application/json': {
+                                    schema: { $ref: '#/components/schemas/info' }
+                                }
+                            }
+                        },
+                    },
+                }
+            },
+            [`${ServerConfig.apiPrefix}/{source}/{id}/box/{a1,a2,a3}/{b1,b2,b3}/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns density data inside the specified box for the given entry. For X-ray data, returns 2Fo-Fc and Fo-Fc volumes in a single response.',
+                    operationId: 'getBox',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                        {
+                            name: 'bottomLeftCorner',
+                            in: 'path',
+                            description: 'Bottom left corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
+                            required: true,
+                            schema: {
+                                type: 'list',
+                                items: {
+                                    type: 'float',
+                                }
+                            },
+                            style: 'simple'
+                        },
+                        {
+                            name: 'topRightCorner',
+                            in: 'path',
+                            description: 'Top right corner of the query region in Cartesian or fractional coordinates (determined by the `space` query parameter).',
+                            required: true,
+                            schema: {
+                                type: 'list',
+                                items: {
+                                    type: 'float',
+                                }
+                            },
+                            style: 'simple'
+                        },
+                        { $ref: '#/components/parameters/encoding' },
+                        { $ref: '#/components/parameters/detail' },
+                        {
+                            name: 'space',
+                            in: 'query',
+                            description: 'Determines the coordinate space the query is in. Can be cartesian or fractional. An optional argument, default values is cartesian.',
+                            schema: {
+                                type: 'string',
+                                enum: ['cartesian', 'fractional']
+                            },
+                            style: 'form'
+                        }
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume box',
+                            content: {
+                                'text/plain': {},
+                                'application/octet-stream': {},
+                            }
+                        },
+                    },
+                }
+            },
+            [`${ServerConfig.apiPrefix}/{source}/{id}/cell/`]: {
+                get: {
+                    tags: ['General'],
+                    summary: 'Returns (downsampled) volume data for the entire "data cell". For X-ray data, returns unit cell of 2Fo-Fc and Fo-Fc volumes, for EM data returns everything.',
+                    operationId: 'getCell',
+                    parameters: [
+                        { $ref: '#/components/parameters/source' },
+                        { $ref: '#/components/parameters/id' },
+                        { $ref: '#/components/parameters/encoding' },
+                        { $ref: '#/components/parameters/detail' },
+                    ],
+                    responses: {
+                        200: {
+                            description: 'Volume cell',
+                            content: {
+                                'text/plain': {},
+                                'application/octet-stream': {},
+                            }
+                        },
+                    },
+                }
+            }
+        },
+        components: {
+            schemas: {
+                // TODO how to keep in sync with (or derive from) `api.ts/ExtendedHeader`
+                info: {
+                    properties: {
+                        formatVersion: {
+                            type: 'string',
+                            description: 'Format version number'
+                        },
+                        axisOrder: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Axis order from the slowest to fastest moving, same as in CCP4'
+                        },
+                        origin: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Origin in fractional coordinates, in axisOrder'
+                        },
+                        dimensions: {
+                            type: 'array',
+                            items: { type: 'number' },
+                            description: 'Dimensions in fractional coordinates, in axisOrder'
+                        },
+                        spacegroup: {
+                            properties: {
+                                number: { type: 'number' },
+                                size: {
+                                    type: 'array',
+                                    items: { type: 'number' }
+                                },
+                                angles: {
+                                    type: 'array',
+                                    items: { type: 'number' }
+                                },
+                                isPeriodic: {
+                                    type: 'boolean',
+                                    description: 'Determine if the data should be treated as periodic or not. (e.g. X-ray = periodic, EM = not periodic)'
+                                },
+                            }
+                        },
+                        channels: {
+                            type: 'array',
+                            items: { type: 'string' }
+                        },
+                        valueType: {
+                            type: 'string',
+                            enum: ['float32', 'int16', 'int8'],
+                            description: 'Determines the data type of the values'
+                        },
+                        blockSize: {
+                            type: 'number',
+                            description: 'The value are stored in blockSize^3 cubes'
+                        },
+                        sampling: {
+                            type: 'array',
+                            items: {
+                                properties: {
+                                    byteOffset: { type: 'number' },
+                                    rate: {
+                                        type: 'number',
+                                        description: 'How many values along each axis were collapsed into 1'
+                                    },
+                                    valuesInfo: {
+                                        properties: {
+                                            mean: { type: 'number' },
+                                            sigma: { type: 'number' },
+                                            min: { type: 'number' },
+                                            max: { type: 'number' },
+                                        }
+                                    },
+                                    sampleCount: {
+                                        type: 'array',
+                                        items: { type: 'number' },
+                                        description: 'Number of samples along each axis, in axisOrder'
+                                    },
+                                }
+                            }
+                        }
+                    }
+                }
+            },
+            parameters: {
+                source: {
+                    name: 'source',
+                    in: 'path',
+                    description: `Specifies the data source (determined by the experiment method). Currently supported sources are: ${sources.join(', ')}.`,
+                    required: true,
+                    schema: {
+                        type: 'string',
+                        enum: sources
+                    },
+                    style: 'simple'
+                },
+                id: {
+                    name: 'id',
+                    in: 'path',
+                    description: 'Id of the entry. For x-ray, use PDB ID (i.e. 1cbs) and for em use EMDB id (i.e. emd-8116).',
+                    required: true,
+                    schema: {
+                        type: 'string',
+                    },
+                    style: 'simple'
+                },
+                encoding: {
+                    name: 'encoding',
+                    in: 'query',
+                    description: 'Determines if text based CIF or binary BinaryCIF encoding is used. An optional argument, default is BinaryCIF encoding.',
+                    schema: {
+                        type: 'string',
+                        enum: ['cif', 'bcif']
+                    },
+                    style: 'form'
+                },
+                detail: {
+                    name: 'detail',
+                    in: 'query',
+                    description: `Determines the maximum number of voxels the query can return. Possible values are in the range from ${detail(0)} to ${detail(detailMax)}. Default value is 0. Note: different detail levels might lead to the same result.`,
+                    schema: {
+                        type: 'integer',
+                    },
+                    style: 'form'
+                }
+            }
+        }
+    }
+}
+
+export const shortcutIconLink = `<link rel='shortcut icon' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAnUExURQAAAMIrHrspHr0oH7soILonHrwqH7onILsoHrsoH7soH7woILwpIKgVokoAAAAMdFJOUwAQHzNxWmBHS5XO6jdtAmoAAACZSURBVDjLxZNRCsQgDAVNXmwb9f7nXZEaLRgXloXOhwQdjMYYwpOLw55fBT46KhbOKhmRR2zLcFJQj8UR+HxFgArIF5BKJbEncC6NDEdI5SatBRSDJwGAoiFDONrEJXWYhGMIcRJGCrb1TOtDahfUuQXd10jkFYq0ViIrbUpNcVT6redeC1+b9tH2WLR93Sx2VCzkv/7NjfABxjQHksGB7lAAAAAASUVORK5CYII=' />`
\ No newline at end of file
diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9eeecd9a090d762b12fb75c563488aad41d40da6
--- /dev/null
+++ b/src/tests/browser/render-lines.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>
+ */
+
+import './index.html'
+import { Canvas3D } from 'mol-canvas3d/canvas3d';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { Representation } from 'mol-repr/representation';
+import { Color } from 'mol-util/color';
+import { createRenderObject } from 'mol-gl/render-object';
+import { Lines } from 'mol-geo/geometry/lines/lines';
+import { LinesBuilder } from 'mol-geo/geometry/lines/lines-builder';
+import { DodecahedronCage } from 'mol-geo/primitive/dodecahedron';
+
+const parent = document.getElementById('app')!
+parent.style.width = '100%'
+parent.style.height = '100%'
+
+const canvas = document.createElement('canvas')
+canvas.style.width = '100%'
+canvas.style.height = '100%'
+parent.appendChild(canvas)
+
+const canvas3d = Canvas3D.create(canvas, parent)
+canvas3d.animate()
+
+function linesRepr() {
+    const linesBuilder = LinesBuilder.create()
+    const t = Mat4.identity()
+    const dodecahedronCage = DodecahedronCage()
+    linesBuilder.addCage(t, dodecahedronCage, 0)
+    const lines = linesBuilder.getLines()
+
+    const values = Lines.Utils.createValuesSimple(lines, {}, Color(0xFF0000), 3)
+    const state = Lines.Utils.createRenderableState({})
+    const renderObject = createRenderObject('lines', values, state)
+    const repr = Representation.fromRenderObject('cage-lines', renderObject)
+    return repr
+}
+
+canvas3d.add(linesRepr())
+canvas3d.resetCamera()
\ No newline at end of file
diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts
index 2e50761d2c91009dee24c80adfbcf7369fc66233..4c63a2eb90fbef2500adcdcfea766fc5d045e153 100644
--- a/src/tests/browser/render-mesh.ts
+++ b/src/tests/browser/render-mesh.ts
@@ -7,12 +7,13 @@
 import './index.html'
 import { Canvas3D } from 'mol-canvas3d/canvas3d';
 import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder';
-import { Sphere } from 'mol-geo/primitive/sphere';
 import { Mat4 } from 'mol-math/linear-algebra';
 import { Mesh } from 'mol-geo/geometry/mesh/mesh';
 import { Representation } from 'mol-repr/representation';
 import { Color } from 'mol-util/color';
 import { createRenderObject } from 'mol-gl/render-object';
+import { SpikedBall } from 'mol-geo/primitive/spiked-ball';
+import { HexagonalPrismCage } from 'mol-geo/primitive/prism';
 
 const parent = document.getElementById('app')!
 parent.style.width = '100%'
@@ -28,15 +29,20 @@ canvas3d.animate()
 
 function meshRepr() {
     const builderState = MeshBuilder.createState()
+    
     const t = Mat4.identity()
-    const sphere = Sphere(2)
-    MeshBuilder.addPrimitive(builderState, t, sphere)
+    MeshBuilder.addCage(builderState, t, HexagonalPrismCage(), 0.005, 2)
+
+    const t2 = Mat4.identity()
+    Mat4.scaleUniformly(t2, t2, 0.1)
+    MeshBuilder.addPrimitive(builderState, t2, SpikedBall(3))
+
     const mesh = MeshBuilder.getMesh(builderState)
 
     const values = Mesh.Utils.createValuesSimple(mesh, {}, Color(0xFF0000), 1)
     const state = Mesh.Utils.createRenderableState({})
     const renderObject = createRenderObject('mesh', values, state)
-    const repr = Representation.fromRenderObject('sphere-mesh', renderObject)
+    const repr = Representation.fromRenderObject('mesh', renderObject)
     return repr
 }
 
diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9dae8a78c7380888fce1cf4db1a64259745cba21
--- /dev/null
+++ b/src/tests/browser/render-structure.ts
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import './index.html'
+import { Canvas3D } from 'mol-canvas3d/canvas3d';
+import CIF, { CifFrame } from 'mol-io/reader/cif'
+import { Model, Structure } from 'mol-model/structure';
+import { ColorTheme } from 'mol-theme/color';
+import { SizeTheme } from 'mol-theme/size';
+import { CartoonRepresentationProvider } from 'mol-repr/structure/representation/cartoon';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
+import { computeModelDSSP } from 'mol-model/structure/model/properties/utils/secondary-structure';
+
+const parent = document.getElementById('app')!
+parent.style.width = '100%'
+parent.style.height = '100%'
+
+const canvas = document.createElement('canvas')
+canvas.style.width = '100%'
+canvas.style.height = '100%'
+parent.appendChild(canvas)
+
+const canvas3d = Canvas3D.create(canvas, parent)
+canvas3d.animate()
+
+
+async function parseCif(data: string|Uint8Array) {
+    const comp = CIF.parse(data);
+    const parsed = await comp.run();
+    if (parsed.isError) throw parsed;
+    return parsed.result;
+}
+
+async function downloadCif(url: string, isBinary: boolean) {
+    const data = await fetch(url);
+    return parseCif(isBinary ? new Uint8Array(await data.arrayBuffer()) : await data.text());
+}
+
+async function downloadFromPdb(pdb: string) {
+    // const parsed = await downloadCif(`https://files.rcsb.org/download/${pdb}.cif`, false);
+    const parsed = await downloadCif(`https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${pdb}`, true);
+    return parsed.blocks[0];
+}
+
+async function getModels(frame: CifFrame) {
+    return await trajectoryFromMmCIF(frame).run();
+}
+
+async function getStructure(model: Model) {
+    return Structure.ofModel(model);
+}
+
+const reprCtx = {
+    colorThemeRegistry: ColorTheme.createRegistry(),
+    sizeThemeRegistry: SizeTheme.createRegistry()
+}
+function getCartoonRepr() {
+    return CartoonRepresentationProvider.factory(reprCtx, CartoonRepresentationProvider.getParams)
+}
+
+async function init() {
+    const cif = await downloadFromPdb('3j3q')
+    const models = await getModels(cif)
+    console.time('computeModelDSSP')
+    const secondaryStructure = computeModelDSSP(models[0].atomicHierarchy, models[0].atomicConformation)
+    console.timeEnd('computeModelDSSP')
+    ;(models[0].properties as any).secondaryStructure = secondaryStructure
+    const structure = await getStructure(models[0])
+    const cartoonRepr = getCartoonRepr()
+
+    cartoonRepr.setTheme({
+        color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }),
+        size: reprCtx.sizeThemeRegistry.create('uniform', { structure })
+    })
+    await cartoonRepr.createOrUpdate({ ...CartoonRepresentationProvider.defaultValues, quality: 'auto' }, structure).run()
+    
+    canvas3d.add(cartoonRepr)
+    canvas3d.resetCamera()
+}
+
+init()
\ No newline at end of file
diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts
index a299d3cefae15e1c68d64b835462b4893e3a31a0..4c453479b34dcd3041d72219f577b1b7e143bbb8 100644
--- a/src/tests/browser/render-text.ts
+++ b/src/tests/browser/render-text.ts
@@ -30,24 +30,29 @@ canvas3d.animate()
 function textRepr() {
     const props: PD.Values<Text.Params> = {
         ...PD.getDefaultValues(Text.Params),
-        attachment: 'middle-center',
+        attachment: 'top-right',
         fontQuality: 3,
         fontWeight: 'normal',
-        borderWidth: 0.3
+        borderWidth: 0.3,
+        background: true,
+        backgroundOpacity: 0.5,
+        tether: true,
+        tetherLength: 1.5,
+        tetherBaseWidth: 0.5,
     }
 
     const textBuilder = TextBuilder.create(props, 1, 1)
-    textBuilder.add('Hello world', 0, 0, 0, 0)
-    textBuilder.add('Добрый день', 0, 1, 0, 0)
-    textBuilder.add('美好的一天', 0, 2, 0, 0)
-    textBuilder.add('¿Cómo estás?', 0, -1, 0, 0)
-    textBuilder.add('αβγ Å', 0, -2, 0, 0)
+    textBuilder.add('Hello world', 0, 0, 0, 1, 0)
+    // textBuilder.add('Добрый день', 0, 1, 0, 0, 0)
+    // textBuilder.add('美好的一天', 0, 2, 0, 0, 0)
+    // textBuilder.add('¿Cómo estás?', 0, -1, 0, 0, 0)
+    // textBuilder.add('αβγ Å', 0, -2, 0, 0, 0)
     const text = textBuilder.getText()
 
     const values = Text.Utils.createValuesSimple(text, props, Color(0xFFDD00), 1)
     const state = Text.Utils.createRenderableState(props)
     const renderObject = createRenderObject('text', values, state)
-    console.log('text', renderObject)
+    console.log('text', renderObject, props)
     const repr = Representation.fromRenderObject('text', renderObject)
     return repr
 }
diff --git a/tsconfig.json b/tsconfig.json
index 70aa52c9b25f08ccd1f961c4cf42d4dc0138d4aa..0a845d1985969692496c7afa252226647952ee33 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,6 +23,7 @@
             "mol-math": ["./mol-math"],
             "mol-model": ["./mol-model"],
             "mol-model-props": ["./mol-model-props", "./mol-model-props/index.ts"],
+            "mol-model-formats": ["./mol-model-formats"],
             "mol-ql": ["./mol-ql"],
             "mol-repr": ["./mol-repr"],
             "mol-script": ["./mol-script"],
diff --git a/webpack.config.js b/webpack.config.js
index f36e7340df2e87cfc75f0514cd92178313679870..bda3612a92f256206750a7c20e5422ed44d57fc9 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,4 +1,5 @@
 const path = require('path');
+const webpack = require('webpack');
 const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 // const CircularDependencyPlugin = require('circular-dependency-plugin');
@@ -9,25 +10,25 @@ const sharedConfig = {
             {
                 loader: 'raw-loader',
                 test: /\.(glsl|frag|vert)$/,
-                include: [ path.resolve(__dirname, 'build/src/') ],
+                include: [path.resolve(__dirname, 'build/src/')],
             },
             {
                 loader: 'glslify-loader',
                 test: /\.(glsl|frag|vert)$/,
-                include: [ path.resolve(__dirname, 'build/src/') ]
+                include: [path.resolve(__dirname, 'build/src/')]
             },
 
             {
                 loader: 'file-loader',
                 test: /\.(woff2?|ttf|otf|eot|svg|html)$/,
-                include: [ path.resolve(__dirname, 'build/src/') ],
+                include: [path.resolve(__dirname, 'build/src/')],
                 options: {
                     name: '[name].[ext]'
                 }
             },
             {
-                test:/\.(s*)css$/,
-                use: [ MiniCssExtractPlugin.loader, 'css-loader', 'resolve-url-loader', 'sass-loader' ]
+                test: /\.(s*)css$/,
+                use: [MiniCssExtractPlugin.loader, 'css-loader', 'resolve-url-loader', 'sass-loader']
             }
         ]
     },
@@ -46,6 +47,9 @@ const sharedConfig = {
                 './build/src/**/*.html'
             ],
         }),
+        new webpack.DefinePlugin({
+            __PLUGIN_VERSION_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true),
+        }),
         new MiniCssExtractPlugin({ filename: 'app.css' })
     ],
     resolve: {
@@ -56,8 +60,29 @@ const sharedConfig = {
     }
 }
 
+
+function createEntry(src, outFolder, outFilename, isNode) {
+    return {
+        node: isNode ? void 0 : { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts
+        target: isNode ? 'node' : void 0,
+        entry: path.resolve(__dirname, `build/src/${src}.js`),
+        output: { filename: `${outFilename}.js`, path: path.resolve(__dirname, `build/${outFolder}`) },
+        ...sharedConfig
+    }
+}
+
 function createEntryPoint(name, dir, out) {
     return {
+        node: { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts
+        entry: path.resolve(__dirname, `build/src/${dir}/${name}.js`),
+        output: { filename: `${name}.js`, path: path.resolve(__dirname, `build/${out}`) },
+        ...sharedConfig
+    }
+}
+
+function createNodeEntryPoint(name, dir, out) {
+    return {
+        target: 'node',
         entry: path.resolve(__dirname, `build/src/${dir}/${name}.js`),
         output: { filename: `${name}.js`, path: path.resolve(__dirname, `build/${out}`) },
         ...sharedConfig
@@ -66,14 +91,21 @@ function createEntryPoint(name, dir, out) {
 
 function createApp(name) { return createEntryPoint('index', `apps/${name}`, name) }
 function createBrowserTest(name) { return createEntryPoint(name, 'tests/browser', 'tests') }
+function createNodeApp(name) { return createNodeEntryPoint('index', `apps/${name}`, name) }
 
 module.exports = [
     createApp('viewer'),
+    createApp('basic-wrapper'),
+    createEntry('examples/proteopedia-wrapper/index', 'examples/proteopedia-wrapper', 'index'),
+    createNodeApp('state-docs'),
+    createNodeEntryPoint('preprocess', 'servers/model', 'model-server'),
     createApp('model-server-query'),
 
     createBrowserTest('font-atlas'),
-    createBrowserTest('render-text'),
+    createBrowserTest('render-lines'),
+    createBrowserTest('render-mesh'),
     createBrowserTest('render-shape'),
     createBrowserTest('render-spheres'),
-    createBrowserTest('render-mesh')
+    createBrowserTest('render-structure'),
+    createBrowserTest('render-text'),
 ]
\ No newline at end of file