Browse Source

Merge branch 'master' into volume

Alexander Rose 6 years ago
parent
commit
83169ca312

+ 4 - 0
README.md

@@ -117,6 +117,10 @@ Run the image
 
     export NODE_PATH="build/src"; node build/src/servers/model/test.js
 
+**State Transformer Docs**
+
+    node build/state-docs
+
 ## Contributing
 Just open an issue or make a pull request. All contributions are welcome.
 

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

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

+ 114 - 0
docs/state/readme.md

@@ -0,0 +1,114 @@
+# 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,
+    // 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).
+
+# 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
+}
+```

+ 670 - 0
docs/state/transforms.md

@@ -0,0 +1,670 @@
+# 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.trajectory-from-mmcif](#ms-plugin-trajectory-from-mmcif)
+* [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-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.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 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-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. If none specified (undefined or empty string), the asymmetric unit is used.)*
+
+### Default Parameters
+```js
+{
+  "id": ""
+}
+```
+----------------------------
+## <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 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'
+      - **isoValue**: Numeric 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**: Numeric value
+
+  - **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'
+      - **isoValue**: Numeric 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": 0.22
+    }
+  },
+  "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": []
+}
+```
+----------------------------

+ 46 - 22
package-lock.json

@@ -1469,7 +1469,7 @@
     },
     "browserify-aes": {
       "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
       "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
       "dev": true,
       "requires": {
@@ -1514,7 +1514,7 @@
     },
     "browserify-rsa": {
       "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+      "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
       "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
       "dev": true,
       "requires": {
@@ -1566,7 +1566,7 @@
     },
     "buffer": {
       "version": "4.9.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
+      "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
       "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
       "dev": true,
       "requires": {
@@ -1933,7 +1933,7 @@
         },
         "strip-ansi": {
           "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "requires": {
@@ -2550,7 +2550,7 @@
     },
     "create-hash": {
       "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
       "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
       "dev": true,
       "requires": {
@@ -2563,7 +2563,7 @@
     },
     "create-hmac": {
       "version": "1.1.7",
-      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
       "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
       "dev": true,
       "requires": {
@@ -2587,7 +2587,7 @@
       "dependencies": {
         "node-fetch": {
           "version": "2.1.2",
-          "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
+          "resolved": "http://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
           "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=",
           "dev": true
         }
@@ -3034,7 +3034,7 @@
     },
     "diffie-hellman": {
       "version": "5.0.3",
-      "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+      "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
       "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
       "dev": true,
       "requires": {
@@ -4281,12 +4281,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -4301,17 +4303,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -4428,7 +4433,8 @@
         "inherits": {
           "version": "2.0.3",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -4440,6 +4446,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -4454,6 +4461,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -4461,12 +4469,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.2.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.1",
             "yallist": "^3.0.0"
@@ -4485,6 +4495,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -4565,7 +4576,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -4577,6 +4589,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -4698,6 +4711,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -7745,7 +7759,7 @@
         },
         "chalk": {
           "version": "1.1.3",
-          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
           "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
           "dev": true,
           "requires": {
@@ -7777,7 +7791,7 @@
         },
         "strip-ansi": {
           "version": "3.0.1",
-          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
           "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
           "dev": true,
           "requires": {
@@ -10974,7 +10988,7 @@
     },
     "sha.js": {
       "version": "2.4.11",
-      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
       "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
       "dev": true,
       "requires": {
@@ -11077,7 +11091,7 @@
     },
     "slice-ansi": {
       "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
+      "resolved": "http://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz",
       "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=",
       "dev": true
     },
@@ -11418,7 +11432,7 @@
         },
         "readable-stream": {
           "version": "2.3.6",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
           "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
           "dev": true,
           "requires": {
@@ -11458,6 +11472,16 @@
         "strip-ansi": "^4.0.0"
       }
     },
+    "string-replace-loader": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-2.1.1.tgz",
+      "integrity": "sha512-0Nvw1LDclF45AFNuYPcD2Jvkv0mwb/dQSnJZMvhqGrT+zzmrpG3OJFD600qfQfNUd5aqfp7fCm2mQMfF7zLbyQ==",
+      "dev": true,
+      "requires": {
+        "loader-utils": "^1.1.0",
+        "schema-utils": "^0.4.5"
+      }
+    },
     "string-width": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
@@ -13959,7 +13983,7 @@
     },
     "whatwg-fetch": {
       "version": "2.0.4",
-      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+      "resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
       "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==",
       "dev": true
     },
@@ -14061,7 +14085,7 @@
         },
         "readable-stream": {
           "version": "2.3.6",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+          "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
           "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
           "dev": true,
           "requires": {

+ 1 - 0
package.json

@@ -104,6 +104,7 @@
     "raw-loader": "^1.0.0",
     "resolve-url-loader": "^3.0.0",
     "sass-loader": "^7.1.0",
+    "string-replace-loader": "^2.1.1",
     "style-loader": "^0.23.1",
     "ts-jest": "^23.10.5",
     "tslint": "^5.12.1",

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

@@ -0,0 +1,44 @@
+<!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* Viewer</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+                box-sizing: border-box;
+            }
+            #app {
+                position: absolute;
+                left: 100px;
+                top: 100px;
+                width: 600px;
+                height: 600px;
+                border: 1px solid #ccc;
+            }
+        </style>
+        <link rel="stylesheet" type="text/css" href="app.css" />
+        <script type="text/javascript" src="./index.js"></script>
+    </head>
+    <body>
+        <button id='spin'>Toggle Spin</button>
+        <button id='asym'>Load Asym Unit</button>
+        <button id='asm'>Load Assemly 1</button>
+        <div id="app"></div>
+        <script>            
+            var pdbId = '5ire', assemblyId= '1';
+            var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
+
+            BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
+            BasicMolStarWrapper.setBackground(0xffffff);
+            BasicMolStarWrapper.loadCif(url, assemblyId);
+            BasicMolStarWrapper.toggleSpin();
+
+            document.getElementById('spin').onclick = () => BasicMolStarWrapper.toggleSpin();
+            document.getElementById('asym').onclick = () => BasicMolStarWrapper.loadCif(url);
+            document.getElementById('asm').onclick = () => BasicMolStarWrapper.loadCif(url, assemblyId);
+        </script>
+    </body>
+</html>

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

@@ -0,0 +1,77 @@
+/**
+ * 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 { StateTree } from 'mol-state';
+import { Color } from 'mol-util/color';
+require('mol-plugin/skin/light.scss')
+
+class BasicWrapper {
+    plugin: PluginContext;
+    stateTemplate: StateTree;
+
+    init(target: string | HTMLElement) {
+        this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
+            ...DefaultPluginSpec,
+            initialLayout: {
+                isExpanded: false,
+                showControls: false
+            }
+        });
+
+        const state = this.plugin.state.dataState.build();
+        const visualRoot = state.toRoot()
+            .apply(StateTransforms.Data.Download, { url: '', isBinary: false }, { ref: 'url' })
+            .apply(StateTransforms.Data.ParseCif)
+            .apply(StateTransforms.Model.TrajectoryFromMmCif)
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: '' }, { ref: 'asm' })
+
+        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'));
+
+        this.stateTemplate = state.getTree();
+    }
+
+    async loadCif(url: string, assemblyId?: string) {
+        const state = this.stateTemplate.build();
+
+        state.to('url').update(StateTransforms.Data.Download, p => ({ ...p, url }));
+        state.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId }));
+
+        await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree: state });
+
+        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, { });
+    }
+}
+
+(window as any).BasicMolStarWrapper = new BasicWrapper();

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

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as _ from 'mol-plugin/state/transforms'
+import { Transformer, 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: Transformer) {
+    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 = Transformer.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));

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

@@ -0,0 +1,71 @@
+/**
+ * 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 '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][]`;
+        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;
+}

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

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

+ 8 - 2
src/apps/viewer/index.ts

@@ -4,8 +4,14 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { createPlugin } from 'mol-plugin';
+import { createPlugin, DefaultPluginSpec } from 'mol-plugin';
 import './index.html'
 require('mol-plugin/skin/light.scss')
 
-createPlugin(document.getElementById('app')!);
+createPlugin(document.getElementById('app')!, {
+    ...DefaultPluginSpec,
+    initialLayout: {
+        isExpanded: true,
+        showControls: true
+    }
+});

+ 4 - 0
src/mol-plugin/command.ts

@@ -9,6 +9,7 @@ import { PluginCommand } from './command/base';
 import { Transform, State } from 'mol-state';
 import { StateAction } from 'mol-state/action';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
+import { PluginLayoutStateProps } from './layout';
 
 export * from './command/base';
 
@@ -38,6 +39,9 @@ export const PluginCommands = {
             OpenFile: PluginCommand<{ file: File }>({ isImmediate: true }),
         }
     },
+    Layout: {
+        Update: PluginCommand<{ state: Partial<PluginLayoutStateProps> }>({ isImmediate: true })
+    },
     Camera: {
         Reset: PluginCommand<{}>({ isImmediate: true }),
         SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>({ isImmediate: true }),

+ 42 - 0
src/mol-plugin/component.ts

@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { BehaviorSubject, Observable, Subject } from 'rxjs';
+import { PluginContext } from './context';
+import { shallowMergeArray } from 'mol-util/object';
+
+export class PluginComponent<State> {
+    private _state: BehaviorSubject<State>;
+    private _updated = new Subject();
+
+    updateState(...states: Partial<State>[]) {
+        const latest = this.latestState;
+        const s = shallowMergeArray(latest, states);
+        if (s !== latest) {
+            this._state.next(s);
+        }
+    }
+
+    get states() {
+        return <Observable<State>>this._state;
+    }
+
+    get latestState() {
+        return this._state.value;
+    }
+
+    get updated() {
+        return <Observable<{}>>this._updated;
+    }
+
+    triggerUpdate() {
+        this._updated.next({});
+    }
+
+    constructor(public context: PluginContext, initialState: State) {
+        this._state = new BehaviorSubject<State>(initialState);
+    }
+}

+ 13 - 1
src/mol-plugin/context.ts

@@ -26,6 +26,9 @@ import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager';
 import { ajaxGet } from 'mol-util/data-source';
 import { CustomPropertyRegistry } from './util/custom-prop-registry';
 import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
+import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
+import { PluginLayout } from './layout';
+import { List } from 'immutable';
 
 export class PluginContext {
     private disposed = false;
@@ -69,6 +72,7 @@ export class PluginContext {
     };
 
     readonly canvas3d: Canvas3D;
+    readonly layout: PluginLayout = new PluginLayout(this);
 
     readonly lociLabels: LociLabelManager;
 
@@ -86,6 +90,8 @@ export class PluginContext {
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
+            this.layout.setRoot(container);
+            if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             this.canvas3d.animate();
@@ -98,6 +104,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)),
@@ -145,7 +152,7 @@ export class PluginContext {
             tree.toRoot().apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
         }
 
-        await this.runTask(this.state.behaviorState.update(tree));
+        await this.runTask(this.state.behaviorState.update(tree, true));
     }
 
     initDataActions() {
@@ -165,12 +172,17 @@ export class PluginContext {
     }
 
     constructor(public spec: PluginSpec) {
+        this.events.log.subscribe(e => this.log.entries = this.log.entries.push(e));
+
         this.initBuiltInBehavior();
 
         this.initBehaviors();
         this.initDataActions();
 
         this.lociLabels = new LociLabelManager(this);
+
+        // TODO: find a better solution for this.
+        setTimeout(() => this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE}]`), 500);
     }
 
     // settings = ;

+ 3 - 3
src/mol-plugin/index.ts

@@ -20,7 +20,7 @@ function getParam(name: string, regex: string): string {
     return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
 }
 
-const DefaultSpec: PluginSpec = {
+export const DefaultPluginSpec: PluginSpec = {
     actions: [
         PluginSpec.Action(DownloadStructure),
         PluginSpec.Action(DownloadDensity),
@@ -50,8 +50,8 @@ const DefaultSpec: PluginSpec = {
     ]
 }
 
-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);

+ 180 - 0
src/mol-plugin/layout.ts

@@ -0,0 +1,180 @@
+/**
+ * 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> {
+    private updateProps(state: Partial<PluginLayoutStateProps>) {
+        let prevExpanded = !!this.latestState.isExpanded;
+        this.updateState(state);
+        if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
+
+        this.triggerUpdate();
+    }
+
+    private root: HTMLElement;
+    private rootState: RootState | undefined = void 0;
+    private expandedViewport: HTMLMetaElement;
+
+    setRoot(root: HTMLElement) {
+        this.root = root;
+        if (this.latestState.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.latestState.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(context: PluginContext) {
+        super(context, { ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout });
+
+        PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
+
+        // <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';
+    }
+}

+ 1 - 1
src/mol-plugin/skin/base/components/controls.scss

@@ -218,7 +218,7 @@
     //border-left-style: solid;
     //border-left-color: color-increase-contrast($default-background, 10%);
 
-    margin-bottom: 1px;
+    margin-bottom: 0px;
     padding-top: 1px;
 }
 

+ 3 - 1
src/mol-plugin/spec.ts

@@ -7,12 +7,14 @@
 import { StateAction } from 'mol-state/action';
 import { Transformer } from 'mol-state';
 import { StateTransformParameters } from './ui/state/common';
+import { PluginLayoutStateProps } from './layout';
 
 export { PluginSpec }
 
 interface PluginSpec {
     actions: PluginSpec.Action[],
-    behaviors: PluginSpec.Behavior[]
+    behaviors: PluginSpec.Behavior[],
+    initialLayout?: PluginLayoutStateProps
 }
 
 namespace PluginSpec {

+ 13 - 11
src/mol-plugin/state.ts

@@ -52,11 +52,13 @@ class PluginState {
     }
 
     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);
+        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.cameraSnapshots) this.cameraSnapshots.setStateSnapshot(snapshot.cameraSnapshots);
+        if (snapshot.canvas3d) {
+            if (snapshot.canvas3d.viewport) PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: snapshot.canvas3d.viewport || { } });
+            if (snapshot.canvas3d.camera) this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
+        }
         this.plugin.canvas3d.requestDraw(true);
     }
 
@@ -86,12 +88,12 @@ namespace PluginState {
     export type Kind = 'data' | 'behavior'
 
     export interface Snapshot {
-        data: State.Snapshot,
-        behaviour: State.Snapshot,
-        cameraSnapshots: CameraSnapshotManager.StateSnapshot,
-        canvas3d: {
-            camera: Camera.Snapshot,
-            viewport: Canvas3DProps
+        data?: State.Snapshot,
+        behaviour?: State.Snapshot,
+        cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
+        canvas3d?: {
+            camera?: Camera.Snapshot,
+            viewport?: Canvas3DProps
         }
     }
 }

+ 9 - 6
src/mol-plugin/state/transforms/model.ts

@@ -105,19 +105,19 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     to: SO.Molecule.Structure,
     params(a) {
         if (!a) {
-            return { id: PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. If none specified (undefined or empty string), the asymmetric unit is used.' }) };
+            return { id: PD.makeOptional(PD.Text('', { label: 'Assembly Id', description: 'Assembly Id. If none specified (undefined or empty string), the asymmetric unit is used.' })) };
         }
         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' }) };
+        return { id: PD.makeOptional(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);
+            const asm = ModelSymmetry.findAssembly(model, id || '');
             if (!!id && id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`);
 
             const base = Structure.ofModel(model);
@@ -138,7 +138,7 @@ 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: {
@@ -157,14 +157,17 @@ const StructureSelection = PluginStateTransform.BuiltIn({
 });
 
 export { StructureComplexElement }
-namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' }
+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.

+ 3 - 3
src/mol-plugin/ui/controls.tsx

@@ -24,15 +24,15 @@ export class TrajectoryControls extends PluginComponent {
             <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>
+            })} title='Previou Model'>◀</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>
+            })} title='First Model'>↻</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 />
+            })} title='Next Model'>►</button><br />
         </div>
     }
 }

+ 24 - 1
src/mol-plugin/ui/controls/common.tsx

@@ -4,12 +4,35 @@
  * @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 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(); }}>

+ 35 - 17
src/mol-plugin/ui/plugin.tsx

@@ -33,22 +33,41 @@ 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 PluginComponent {
+    componentDidMount() {
+        this.subscribe(this.plugin.layout.updated, () => this.forceUpdate());
+    }
+
+    region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
+        return <div className={`msp-layout-region msp-layout-${kind}`}>
+            <div className='msp-layout-static'>
+                {element}
+            </div>
+        </div>;
+    }
+
+    render() {
+        const layout = this.plugin.layout.latestState;
+        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 && this.region('left', <State />)}
+                    {layout.showControls && this.region('right', <div className='msp-scrollable-container msp-right-controls'>
+                        <CurrentObject />
+                        <Controls />
+                        <CameraSnapshots />
+                        <StateSnapshots />
+                    </div>)}
+                    {layout.showControls && this.region('bottom', <Log />)}
                 </div>
             </div>
-        </PluginReactContext.Provider>;
+        </div>;
     }
 }
 
@@ -96,15 +115,14 @@ export class Log extends PluginComponent<{}, { 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.takeLast(100).toList() }));
     }
 
     componentDidUpdate() {
         this.scrollToBottom();
     }
 
-    state = { entries: List<LogEntry>() };
+    state = { entries: this.plugin.log.entries.takeLast(100).toList() };
 
     private scrollToBottom() {
         const log = this.wrapper.current;

+ 43 - 14
src/mol-plugin/ui/viewport.tsx

@@ -13,6 +13,8 @@ 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 } from './controls/common';
 
 interface ViewportState {
     noWebGl: boolean
@@ -20,8 +22,7 @@ interface ViewportState {
 
 export class ViewportControls extends PluginComponent {
     state = {
-        isSettingsExpanded: false,
-        settings: PD.getDefaultValues(Canvas3DParams)
+        isSettingsExpanded: false
     }
 
     resetCamera = () => {
@@ -33,34 +34,58 @@ 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.latestState.showControls } });
+    }
+
+    toggleExpanded = () => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.latestState.isExpanded } });
+    }
 
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { [p.name]: p.value } });
     }
 
-    componentDidMount() {
-        if (this.plugin.canvas3d) {
-            this.setState({ settings: this.plugin.canvas3d.props });
-        }
+    setLayout = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { [p.name]: p.value } });
+    }
 
+    componentDidMount() {
         this.subscribe(this.plugin.events.canvad3d.settingsUpdated, e => {
-            this.setState({ settings: this.plugin.canvas3d.props });
+            this.forceUpdate();
         });
+
+        this.subscribe(this.plugin.layout.updated, () => {
+            this.forceUpdate();
+        });
+    }
+
+    icon(name: string, onClick: (e: React.MouseEvent<HTMLButtonElement>) => void, title: string, isOn = true) {
+        return <button
+            className={`msp-btn msp-btn-link msp-btn-link-toggle-${isOn ? 'on' : 'off'}`}
+            onClick={onClick}
+            title={title}>
+                <span className={`msp-icon msp-icon-${name}`}/>
+            </button>
     }
 
     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('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.latestState.showControls)}
+                {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.latestState.isExpanded)}
+                {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
+                {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}
             </div>
             {this.state.isSettingsExpanded &&
             <div className='msp-viewport-controls-scene-options'>
-                <ParameterControls params={Canvas3DParams} values={this.state.settings} onChange={this.setSettings} />
+                <ControlGroup header='Layout' initialExpanded={true}>
+                    <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} />
+                </ControlGroup>
+                <ControlGroup header='Viewport' initialExpanded={true}>
+                    <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
+                </ControlGroup>
             </div>}
         </div>
     }
@@ -75,7 +100,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
     };
 
     private handleResize = () => {
-         this.plugin.canvas3d.handleResize();
+        this.plugin.canvas3d.handleResize();
     }
 
     componentDidMount() {
@@ -102,6 +127,10 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
             if (buttons !== ButtonsType.Flag.Primary) return;
             idHelper.select(x, y);
         });
+
+        this.subscribe(this.plugin.layout.updated, () => {
+            setTimeout(this.handleResize, 50);
+        });
     }
 
     componentWillUnmount() {

+ 8 - 0
src/mol-plugin/version.ts

@@ -0,0 +1,8 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export const PLUGIN_VERSION = '0.5.0';
+export const PLUGIN_VERSION_DATE = '$PLUGIN_VERSION_DATE$';

+ 9 - 4
src/mol-state/state.ts

@@ -118,7 +118,7 @@ class State {
         });
     }
 
-    update(tree: StateTree | StateTreeBuilder): Task<void> {
+    update(tree: StateTree | StateTreeBuilder, silent: boolean = false): Task<void> {
         const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
         return Task.create('Update Tree', async taskCtx => {
             let updated = false;
@@ -139,6 +139,8 @@ class State {
 
                     results: [],
 
+                    silent,
+
                     changed: false,
                     hadError: false,
                     newCurrent: void 0
@@ -209,6 +211,9 @@ interface UpdateContext {
 
     results: UpdateNodeResult[],
 
+    // suppress timing messages
+    silent: boolean,
+
     changed: boolean,
     hadError: boolean,
     newCurrent?: Ref
@@ -492,13 +497,13 @@ 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.silent) 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.silent) 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.silent) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
         }
     } catch (e) {
         ctx.changed = true;

+ 6 - 2
src/mol-state/transformer.ts

@@ -89,7 +89,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 +105,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) {
@@ -196,7 +200,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; }
     })

+ 1 - 1
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;

+ 4 - 0
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;
 

+ 3 - 1
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 {

+ 26 - 6
webpack.config.js

@@ -9,26 +9,34 @@ 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']
+            },
+            {
+                test: /version\.js$/,
+                loader: 'string-replace-loader',
+                options: {
+                  search: '$PLUGIN_VERSION_DATE$',
+                  replace: function (date) { return date.getFullYear() + '/' + (date.getMonth() + 1) + '/' + date.getDate() } (new Date()),
+                }
+              }
         ]
     },
     plugins: [
@@ -64,11 +72,23 @@ function createEntryPoint(name, dir, out) {
     }
 }
 
+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
+    }
+}
+
 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'),
+    createNodeApp('state-docs'),
     createApp('model-server-query'),
 
     createBrowserTest('font-atlas'),