Browse Source

Merge branch 'master' into sec-struc

Alexander Rose 6 years ago
parent
commit
19520b0f5c
100 changed files with 3554 additions and 2149 deletions
  1. 0 5
      .vscode/tasks.json
  2. 6 0
      docs/state/readme.md
  3. 33 5
      docs/state/transforms.md
  4. 352 394
      package-lock.json
  5. 15 15
      package.json
  6. 65 8
      src/apps/basic-wrapper/index.html
  7. 17 5
      src/apps/basic-wrapper/index.ts
  8. 3 3
      src/apps/state-docs/index.ts
  9. 1 1
      src/apps/structure-info/volume.ts
  10. 4 3
      src/mol-canvas3d/canvas3d.ts
  11. 15 13
      src/mol-canvas3d/controls/trackball.ts
  12. 108 22
      src/mol-io/common/file-handle.ts
  13. 127 0
      src/mol-io/common/simple-buffer.ts
  14. 73 0
      src/mol-io/common/typed-array.ts
  15. 110 68
      src/mol-io/reader/ccp4/parser.ts
  16. 4 1
      src/mol-io/reader/ccp4/schema.ts
  17. 77 51
      src/mol-io/reader/dsn6/parser.ts
  18. 1 1
      src/mol-io/reader/dsn6/schema.ts
  19. 29 14
      src/mol-model-formats/volume/ccp4.ts
  20. 16 20
      src/mol-model/volume/data.ts
  21. 38 7
      src/mol-plugin/behavior/behavior.ts
  22. 2 2
      src/mol-plugin/behavior/dynamic/animation.ts
  23. 1 0
      src/mol-plugin/behavior/dynamic/camera.ts
  24. 1 0
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  25. 1 0
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
  26. 2 2
      src/mol-plugin/behavior/dynamic/labels.ts
  27. 4 2
      src/mol-plugin/behavior/dynamic/representation.ts
  28. 149 0
      src/mol-plugin/behavior/dynamic/volume.ts
  29. 24 6
      src/mol-plugin/behavior/static/state.ts
  30. 9 10
      src/mol-plugin/command.ts
  31. 19 21
      src/mol-plugin/component.ts
  32. 22 8
      src/mol-plugin/context.ts
  33. 15 7
      src/mol-plugin/index.ts
  34. 14 6
      src/mol-plugin/layout.ts
  35. 0 1
      src/mol-plugin/providers/custom-prop.ts
  36. 0 1
      src/mol-plugin/providers/theme.ts
  37. 18 31
      src/mol-plugin/skin/base/components/controls.scss
  38. 4 0
      src/mol-plugin/skin/base/components/misc.scss
  39. 1 0
      src/mol-plugin/skin/base/components/slider.scss
  40. 34 2
      src/mol-plugin/skin/base/components/temp.scss
  41. 41 8
      src/mol-plugin/skin/base/components/transformer.scss
  42. 8 7
      src/mol-plugin/spec.ts
  43. 11 2
      src/mol-plugin/state.ts
  44. 13 0
      src/mol-plugin/state/actions.ts
  45. 0 391
      src/mol-plugin/state/actions/basic.ts
  46. 180 0
      src/mol-plugin/state/actions/structure.ts
  47. 314 0
      src/mol-plugin/state/actions/volume.ts
  48. 79 0
      src/mol-plugin/state/animation/built-in.ts
  49. 192 0
      src/mol-plugin/state/animation/manager.ts
  50. 49 0
      src/mol-plugin/state/animation/model.ts
  51. 14 18
      src/mol-plugin/state/camera.ts
  52. 3 3
      src/mol-plugin/state/objects.ts
  53. 10 15
      src/mol-plugin/state/snapshots.ts
  54. 2 0
      src/mol-plugin/state/transforms.ts
  55. 6 6
      src/mol-plugin/state/transforms/data.ts
  56. 44 112
      src/mol-plugin/state/transforms/model.ts
  57. 26 21
      src/mol-plugin/state/transforms/representation.ts
  58. 195 0
      src/mol-plugin/state/transforms/volume.ts
  59. 2 2
      src/mol-plugin/ui/base.tsx
  60. 5 5
      src/mol-plugin/ui/camera.tsx
  61. 5 5
      src/mol-plugin/ui/controls.tsx
  62. 55 0
      src/mol-plugin/ui/controls/common.tsx
  63. 17 41
      src/mol-plugin/ui/controls/parameters.tsx
  64. 66 27
      src/mol-plugin/ui/controls/slider.tsx
  65. 49 41
      src/mol-plugin/ui/plugin.tsx
  66. 6 6
      src/mol-plugin/ui/state.tsx
  67. 35 0
      src/mol-plugin/ui/state/actions.tsx
  68. 49 0
      src/mol-plugin/ui/state/animation.tsx
  69. 4 5
      src/mol-plugin/ui/state/apply-action.tsx
  70. 23 15
      src/mol-plugin/ui/state/common.tsx
  71. 113 27
      src/mol-plugin/ui/state/tree.tsx
  72. 24 11
      src/mol-plugin/ui/state/update-transform.tsx
  73. 3 3
      src/mol-plugin/ui/task.tsx
  74. 10 10
      src/mol-plugin/ui/viewport.tsx
  75. 1 1
      src/mol-plugin/util/canvas3d-identify.ts
  76. 1 5
      src/mol-repr/representation.ts
  77. 33 27
      src/mol-repr/volume/isosurface.ts
  78. 12 6
      src/mol-state/action.ts
  79. 31 7
      src/mol-state/action/manager.ts
  80. 5 2
      src/mol-state/index.ts
  81. 6 7
      src/mol-state/object.ts
  82. 47 54
      src/mol-state/state.ts
  83. 125 0
      src/mol-state/state/builder.ts
  84. 7 7
      src/mol-state/state/selection.ts
  85. 21 10
      src/mol-state/transform.ts
  86. 9 6
      src/mol-state/transformer.ts
  87. 0 104
      src/mol-state/tree/builder.ts
  88. 24 30
      src/mol-state/tree/immutable.ts
  89. 40 27
      src/mol-state/tree/transient.ts
  90. 2 3
      src/mol-theme/color.ts
  91. 2 1
      src/mol-theme/size.ts
  92. 2 0
      src/mol-util/index.ts
  93. 53 0
      src/mol-util/lru-cache.ts
  94. 3 2
      src/servers/volume/common/binary-schema.ts
  95. 6 30
      src/servers/volume/common/data-format.ts
  96. 5 112
      src/servers/volume/common/file.ts
  97. 25 5
      src/servers/volume/pack.ts
  98. 0 163
      src/servers/volume/pack/ccp4.ts
  99. 16 13
      src/servers/volume/pack/data-model.ts
  100. 11 11
      src/servers/volume/pack/downsampling.ts

+ 0 - 5
.vscode/tasks.json

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

+ 6 - 0
docs/state/readme.md

@@ -8,6 +8,8 @@ interface Snapshot {
     data?: State.Snapshot,
     // Snapshot of behavior state tree
     behaviour?: State.Snapshot,
+    // Snapshot for current animation,
+    animation?: PluginAnimationManager.Snapshot,
     // Saved camera positions
     cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
     canvas3d?: {
@@ -69,6 +71,10 @@ interface Transform.Props {
 
 "Built-in" data state transforms and description of their parameters are defined in ``mol-plugin/state/transforms``. Behavior transforms are defined in ``mol-plugin/behavior``. Auto-generated documentation for the transforms is also [available](transforms.md).
 
+# Animation State
+
+Defined by ``CameraSnapshotManager.StateSnapshot`` in ``mol-plugin/state/animation/manager.ts``.
+
 # Canvas3D State
 
 Defined by ``Canvas3DParams`` in ``mol-canvas3d/canvas3d.ts``.

+ 33 - 5
docs/state/transforms.md

@@ -7,9 +7,11 @@
 * [ms-plugin.parse-ccp4](#ms-plugin-parse-ccp4)
 * [ms-plugin.parse-dsn6](#ms-plugin-parse-dsn6)
 * [ms-plugin.trajectory-from-mmcif](#ms-plugin-trajectory-from-mmcif)
+* [ms-plugin.trajectory-from-pdb](#ms-plugin-trajectory-from-pdb)
 * [ms-plugin.model-from-trajectory](#ms-plugin-model-from-trajectory)
 * [ms-plugin.structure-from-model](#ms-plugin-structure-from-model)
 * [ms-plugin.structure-assembly-from-model](#ms-plugin-structure-assembly-from-model)
+* [ms-plugin.structure-symmetry-from-model](#ms-plugin-structure-symmetry-from-model)
 * [ms-plugin.structure-selection](#ms-plugin-structure-selection)
 * [ms-plugin.structure-complex-element](#ms-plugin-structure-complex-element)
 * [ms-plugin.custom-model-properties](#ms-plugin-custom-model-properties)
@@ -65,7 +67,7 @@
 
 ----------------------------
 ## <a name="ms-plugin-parse-ccp4"></a>ms-plugin.parse-ccp4 :: Binary -> Ccp4
-*Parse CCP4/MRC from Binary data*
+*Parse CCP4/MRC/MAP from Binary data*
 
 ----------------------------
 ## <a name="ms-plugin-parse-dsn6"></a>ms-plugin.parse-dsn6 :: Binary -> Dsn6
@@ -82,6 +84,9 @@
 ```js
 {}
 ```
+----------------------------
+## <a name="ms-plugin-trajectory-from-pdb"></a>ms-plugin.trajectory-from-pdb :: String -> Trajectory
+
 ----------------------------
 ## <a name="ms-plugin-model-from-trajectory"></a>ms-plugin.model-from-trajectory :: Trajectory -> Model
 *Create a molecular structure from the specified model.*
@@ -104,13 +109,36 @@
 *Create a molecular structure assembly.*
 
 ### Parameters
-- **id**?: String *(Assembly Id. If none specified (undefined or empty string), the asymmetric unit is used.)*
+- **id**?: String *(Assembly Id. Value 'deposited' can be used to specify deposited asymmetric unit.)*
 
 ### Default Parameters
 ```js
 {}
 ```
 ----------------------------
+## <a name="ms-plugin-structure-symmetry-from-model"></a>ms-plugin.structure-symmetry-from-model :: Model -> Structure
+*Create a molecular structure symmetry.*
+
+### Parameters
+- **ijkMin**: 3D vector [x, y, z]
+- **ijkMax**: 3D vector [x, y, z]
+
+### Default Parameters
+```js
+{
+  "ijkMin": [
+    -1,
+    -1,
+    -1
+  ],
+  "ijkMax": [
+    1,
+    1,
+    1
+  ]
+}
+```
+----------------------------
 ## <a name="ms-plugin-structure-selection"></a>ms-plugin.structure-selection :: Structure -> Structure
 *Create a molecular structure from the specified query expression.*
 
@@ -149,7 +177,7 @@
 ```
 ----------------------------
 ## <a name="ms-plugin-volume-from-ccp4"></a>ms-plugin.volume-from-ccp4 :: Ccp4 -> Data
-*Create Volume from CCP4/MRC data*
+*Create Volume from CCP4/MRC/MAP data*
 
 ### Parameters
 - **voxelSize**: 3D vector [x, y, z]
@@ -294,7 +322,7 @@ Object with:
       - **highlightColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
-      - **isoValue**: Numeric value
+      - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)*
       - **renderMode**: One of 'isosurface', 'volume'
       - **controlPoints**: A list of 2d vectors [xi, yi][]
       - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
@@ -480,7 +508,7 @@ Object with:
       - **highlightColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
-      - **isoValue**: Numeric value
+      - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)*
       - **renderMode**: One of 'isosurface', 'volume'
       - **controlPoints**: A list of 2d vectors [xi, yi][]
       - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'

File diff suppressed because it is too large
+ 352 - 394
package-lock.json


+ 15 - 15
package.json

@@ -18,7 +18,7 @@
     "watch": "concurrently --kill-others \"npm:watch-ts\" \"npm:watch-extra\" \"npm:watch-webpack\"",
     "watch-ts": "tsc -watch",
     "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --watch",
-    "build-webpack": "webpack --mode development",
+    "build-webpack": "webpack --mode production",
     "watch-webpack": "webpack -w --mode development",
     "model-server": "node build/src/servers/model/server.js",
     "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js"
@@ -79,11 +79,11 @@
     "@types/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.1",
-    "@types/jest": "^23.3.14",
-    "@types/node": "^10.12.23",
-    "@types/node-fetch": "^2.1.4",
-    "@types/react": "^16.8.2",
-    "@types/react-dom": "^16.8.0",
+    "@types/jest": "^24.0.6",
+    "@types/node": "^11.9.4",
+    "@types/node-fetch": "^2.1.6",
+    "@types/react": "^16.8.4",
+    "@types/react-dom": "^16.8.2",
     "@types/webgl2": "0.0.4",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
@@ -96,22 +96,22 @@
     "glslify-import": "^3.1.0",
     "glslify-loader": "^2.0.0",
     "graphql-code-generator": "^0.16.1",
-    "graphql-codegen-time": "^0.16.0",
+    "graphql-codegen-time": "^0.16.1",
     "graphql-codegen-typescript-template": "^0.16.1",
-    "jest": "^23.6.0",
+    "jest": "^24.1.0",
     "jest-raw-loader": "^1.0.1",
     "mini-css-extract-plugin": "^0.5.0",
     "node-sass": "^4.11.0",
     "raw-loader": "^1.0.0",
-    "resolve-url-loader": "^3.0.0",
+    "resolve-url-loader": "^3.0.1",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
-    "ts-jest": "^23.10.5",
+    "ts-jest": "^24.0.0",
     "tslint": "^5.12.1",
-    "typescript": "^3.3.1",
+    "typescript": "^3.3.3",
     "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.29.3",
+    "webpack": "^4.29.5",
     "webpack-cli": "^3.2.3"
   },
   "dependencies": {
@@ -121,8 +121,8 @@
     "graphql": "^14.1.1",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
-    "react": "^16.7.0",
-    "react-dom": "^16.7.0",
-    "rxjs": "^6.3.3"
+    "react": "^16.8.2",
+    "react-dom": "^16.8.2",
+    "rxjs": "^6.4.0"
   }
 }

+ 65 - 8
src/apps/basic-wrapper/index.html

@@ -12,37 +12,94 @@
             }
             #app {
                 position: absolute;
-                left: 100px;
+                left: 160px;
                 top: 100px;
                 width: 600px;
                 height: 600px;
                 border: 1px solid #ccc;
             }
+
+            #controls {
+                position: absolute;
+                width: 130px;
+                top: 10px;
+                left: 10px;
+            }
+
+            #controls > button {
+                display: block;
+                width: 100%;
+                text-align: left;
+            }
+
+            #controls > hr {
+                margin: 5px 0;
+            }
         </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='controls'>
+
+        </div>
         <div id="app"></div>
         <script>            
-            var pdbId = '5ire', assemblyId= '1';
+            var pdbId = '1grm', assemblyId= '1';
             var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
             var format = 'cif';
 
             // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
             // var format = 'pdb';
+            // var assemblyId = 'deposited';
 
             BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
             BasicMolStarWrapper.setBackground(0xffffff);
             BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
             BasicMolStarWrapper.toggleSpin();
 
-            document.getElementById('spin').onclick = () => BasicMolStarWrapper.toggleSpin();
-            document.getElementById('asym').onclick = () => BasicMolStarWrapper.load({ url: url, format: format });
-            document.getElementById('asm').onclick = () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
+            addHeader('Source');
+            addControl('Load Asym Unit', () => BasicMolStarWrapper.load({ url: url, format: format }));
+            addControl('Load Assembly 1', () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }));
+
+            addSeparator();
+
+            addHeader('Camera');
+            addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin());
+            
+            addSeparator();
+
+            addHeader('Animation');
+
+            // adjust this number to make the animation faster or slower
+            // requires to "restart" the animation if changed
+            BasicMolStarWrapper.animate.modelIndex.maxFPS = 4;
+
+            addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward());
+            addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward());
+            addControl('Play Palindrome', () => BasicMolStarWrapper.animate.modelIndex.palindrome());
+            addControl('Play Loop', () => BasicMolStarWrapper.animate.modelIndex.loop());
+            addControl('Stop', () => BasicMolStarWrapper.animate.modelIndex.stop());
+
+            ////////////////////////////////////////////////////////
+
+            function addControl(label, action) {
+                var btn = document.createElement('button');
+                btn.onclick = action;
+                btn.innerText = label;
+                document.getElementById('controls').appendChild(btn);
+            }
+
+            function addSeparator() {
+                var hr = document.createElement('hr');
+                document.getElementById('controls').appendChild(hr);
+            }
+
+            function addHeader(header) {
+                var h = document.createElement('h3');
+                h.innerText = header;
+                document.getElementById('controls').appendChild(h);
+            }
         </script>
     </body>
 </html>

+ 17 - 5
src/apps/basic-wrapper/index.ts

@@ -11,8 +11,9 @@ import { PluginCommands } from 'mol-plugin/command';
 import { StateTransforms } from 'mol-plugin/state/transforms';
 import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
 import { Color } from 'mol-util/color';
-import { StateTreeBuilder } from 'mol-state/tree/builder';
 import { PluginStateObject as PSO } from 'mol-plugin/state/objects';
+import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
+import { StateBuilder } from 'mol-state';
 require('mol-plugin/skin/light.scss')
 
 type SupportedFormats = 'cif' | 'pdb'
@@ -31,11 +32,11 @@ class BasicWrapper {
         });
     }
 
-    private download(b: StateTreeBuilder.To<PSO.Root>, url: string) {
+    private download(b: StateBuilder.To<PSO.Root>, url: string) {
         return b.apply(StateTransforms.Data.Download, { url, isBinary: false })
     }
 
-    private parse(b: StateTreeBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
+    private parse(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
         const parsed = format === 'cif'
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
@@ -45,7 +46,7 @@ class BasicWrapper {
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 
-    private visual(visualRoot: StateTreeBuilder.To<PSO.Molecule.Structure>) {
+    private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
         visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
             .apply(StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon'));
@@ -73,7 +74,7 @@ class BasicWrapper {
             if (state.select('asm').length > 0) loadType = 'update';
         }
 
-        let tree: StateTreeBuilder.Root;
+        let tree: StateBuilder.Root;
         if (loadType === 'full') {
             await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
             tree = state.build();
@@ -98,6 +99,17 @@ class BasicWrapper {
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
         if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { });
     }
+
+    animate = {
+        modelIndex: {
+            maxFPS: 8,
+            onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) },
+            onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) },
+            palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) },
+            loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) },
+            stop: () => this.plugin.state.animation.stop()
+        }
+    }
 }
 
 (window as any).BasicMolStarWrapper = new BasicWrapper();

+ 3 - 3
src/apps/state-docs/index.ts

@@ -5,7 +5,7 @@
  */
 
 import * as _ from 'mol-plugin/state/transforms'
-import { Transformer, StateObject } from 'mol-state';
+import { StateTransformer, StateObject } from 'mol-state';
 import { StringBuilder } from 'mol-util';
 import * as fs from 'fs';
 import { paramsToMd } from './pd-to-md';
@@ -28,7 +28,7 @@ function typeToString(o: StateObject.Ctor[]) {
     return o.map(o => o.name).join(' | ');
 }
 
-function writeTransformer(t: Transformer) {
+function writeTransformer(t: StateTransformer) {
     StringBuilder.write(builder, `## <a name="${t.id.replace('.', '-')}"></a>${t.id} :: ${typeToString(t.definition.from)} -> ${typeToString(t.definition.to)}`);
     StringBuilder.newline(builder);
     if (t.definition.display.description) {
@@ -52,7 +52,7 @@ function writeTransformer(t: Transformer) {
     StringBuilder.newline(builder);
 }
 
-const transformers = Transformer.getAll();
+const transformers = StateTransformer.getAll();
 
 StringBuilder.write(builder, '# Mol* Plugin State Transformer Reference');
 StringBuilder.newline(builder);

+ 1 - 1
src/apps/structure-info/volume.ts

@@ -40,7 +40,7 @@ function print(data: Volume) {
 }
 
 async function doMesh(data: Volume, filename: string) {
-    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.absolute(data.volume.dataStats, 1.5) } )).run();
+    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.absolute(1.5) } )).run();
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
     // Export the mesh in OBJ format.

+ 4 - 3
src/mol-canvas3d/canvas3d.ts

@@ -192,7 +192,7 @@ namespace Canvas3D {
             if (isIdentifying || isUpdating) return false
 
             let didRender = false
-            controls.update()
+            controls.update(currentTime);
             // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
             if (!camera.transition.inTransition) setClipping();
             const cameraChanged = camera.updateMatrices();
@@ -230,6 +230,7 @@ namespace Canvas3D {
         }
 
         let forceNextDraw = false;
+        let currentTime = 0;
 
         function draw(force?: boolean) {
             if (render('draw', !!force || forceNextDraw)) {
@@ -246,8 +247,8 @@ namespace Canvas3D {
         }
 
         function animate() {
-            const t = now();
-            camera.transition.tick(t);
+            currentTime = now();
+            camera.transition.tick(currentTime);
             draw(false)
             window.requestAnimationFrame(animate)
         }

+ 15 - 13
src/mol-canvas3d/controls/trackball.ts

@@ -39,12 +39,12 @@ interface TrackballControls {
     readonly props: Readonly<TrackballControlsProps>
     setProps: (props: Partial<TrackballControlsProps>) => void
 
-    update: () => void
+    update: (t: number) => void
     reset: () => void
     dispose: () => void
 }
 namespace TrackballControls {
-    export function create (input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
+    export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
         const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props }
 
         const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 }
@@ -131,7 +131,7 @@ namespace TrackballControls {
                 Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
 
                 angle *= p.rotateSpeed;
-                Quat.setAxisAngle(rotQuat, rotAxis, angle )
+                Quat.setAxisAngle(rotQuat, rotAxis, angle)
 
                 Vec3.transformQuat(_eye, _eye, rotQuat)
                 Vec3.transformQuat(object.up, object.up, rotQuat)
@@ -150,7 +150,7 @@ namespace TrackballControls {
             Vec2.copy(_movePrev, _moveCurr)
         }
 
-        function zoomCamera () {
+        function zoomCamera() {
             const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed
             if (factor !== 1.0 && factor > 0.0) {
                 Vec3.scale(_eye, _eye, factor)
@@ -207,8 +207,12 @@ namespace TrackballControls {
             }
         }
 
+        let lastUpdated = -1;
         /** Update the object's position, direction and up vectors */
-        function update() {
+        function update(t: number) {
+            if (lastUpdated === t) return;
+            if (p.spin) spin(t - lastUpdated);
+
             Vec3.sub(_eye, object.position, target)
 
             rotateCamera()
@@ -224,6 +228,8 @@ namespace TrackballControls {
             if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) {
                 Vec3.copy(lastPosition, object.position)
             }
+
+            lastUpdated = t;
         }
 
         /** Reset object's vectors and the target vector to their initial values */
@@ -297,25 +303,21 @@ namespace TrackballControls {
         }
 
         const _spinSpeed = Vec2.create(0.005, 0);
-        function spin() {
-            _spinSpeed[0] = (p.spinSpeed || 0) / 1000;
+        function spin(deltaT: number) {
+            const frameSpeed = (p.spinSpeed || 0) / 1000;
+            _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
             if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed);
-            if (p.spin) requestAnimationFrame(spin);
         }
 
         // force an update at start
-        update();
-
-        if (props.spin) { spin(); }
+        update(0);
 
         return {
             viewport,
 
             get props() { return p as Readonly<TrackballControlsProps> },
             setProps: (props: Partial<TrackballControlsProps>) => {
-                const wasSpinning = p.spin
                 Object.assign(p, props)
-                if (p.spin && !wasSpinning) requestAnimationFrame(spin)
             },
 
             update,

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

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

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

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

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

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

+ 110 - 68
src/mol-io/reader/ccp4/parser.ts

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

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

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

+ 77 - 51
src/mol-io/reader/dsn6/parser.ts

@@ -8,6 +8,9 @@ import { Task, RuntimeContext } from 'mol-task';
 import { Dsn6File, Dsn6Header } from './schema'
 import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+
+export const dsn6HeaderSize = 512;
 
 function parseBrixHeader(str: string): Dsn6Header {
     return {
@@ -32,61 +35,58 @@ function parseBrixHeader(str: string): Dsn6Header {
     }
 }
 
-function parseDsn6Header(int: Int16Array): Dsn6Header {
-    const factor = 1 / int[ 17 ]
+function parseDsn6Header(buffer: SimpleBuffer, littleEndian: boolean): Dsn6Header {
+    const readInt = littleEndian ? (o: number) => buffer.readInt16LE(o * 2) : (o: number) => buffer.readInt16BE(o * 2)
+    const factor = 1 / readInt(17)
     return {
-        xStart: int[ 0 ],
-        yStart: int[ 1 ],
-        zStart: int[ 2 ],
-        xExtent: int[ 3 ],
-        yExtent: int[ 4 ],
-        zExtent: int[ 5 ],
-        xRate: int[ 6 ],
-        yRate: int[ 7 ],
-        zRate: int[ 8 ],
-        xlen: int[ 9 ] * factor,
-        ylen: int[ 10 ] * factor,
-        zlen: int[ 11 ] * factor,
-        alpha: int[ 12 ] * factor,
-        beta: int[ 13 ] * factor,
-        gamma: int[ 14 ] * factor,
-        divisor: int[ 15 ] / 100,
-        summand: int[ 16 ],
+        xStart: readInt(0),
+        yStart: readInt(1),
+        zStart: readInt(2),
+        xExtent: readInt(3),
+        yExtent: readInt(4),
+        zExtent: readInt(5),
+        xRate: readInt(6),
+        yRate: readInt(7),
+        zRate: readInt(8),
+        xlen: readInt(9) * factor,
+        ylen: readInt(10) * factor,
+        zlen: readInt(11) * factor,
+        alpha: readInt(12) * factor,
+        beta: readInt(13) * factor,
+        gamma: readInt(14) * factor,
+        divisor: readInt(15) / 100,
+        summand: readInt(16),
         sigma: undefined
     }
 }
 
-async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Result<Dsn6File>> {
-    await ctx.update({ message: 'Parsing DSN6/BRIX file...' });
-
-    const { buffer } = await file.readBuffer(0, file.length)
-    const bin = buffer.buffer
+function getBlocks(header: Dsn6Header) {
+    const { xExtent, yExtent, zExtent } = header
+    const xBlocks = Math.ceil(xExtent / 8)
+    const yBlocks = Math.ceil(yExtent / 8)
+    const zBlocks = Math.ceil(zExtent / 8)
+    return { xBlocks, yBlocks, zBlocks }
+}
 
-    const intView = new Int16Array(bin)
-    const byteView = new Uint8Array(bin)
-    const brixStr = String.fromCharCode.apply(null, byteView.subarray(0, 512))
+export async function readDsn6Header(file: FileHandle): Promise<{ header: Dsn6Header, littleEndian: boolean }> {
+    const { buffer } = await file.readBuffer(0, dsn6HeaderSize)
+    const brixStr = String.fromCharCode.apply(null, buffer) as string
     const isBrix = brixStr.startsWith(':-)')
+    const littleEndian = isBrix || buffer.readInt16LE(18 * 2) === 100
+    const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(buffer, littleEndian)
+    return { header, littleEndian }
+}
 
-    if (!isBrix) {
-        // for DSN6, swap byte order when big endian
-        if (intView[18] !== 100) {
-            for (let i = 0, n = intView.length; i < n; ++i) {
-                const val = intView[i]
-                intView[i] = ((val & 0xff) << 8) | ((val >> 8) & 0xff)
-            }
-        }
+export async function parseDsn6Values(header: Dsn6Header, source: Uint8Array, target: Float32Array, littleEndian: boolean) {
+    if (!littleEndian) {
+        // even though the values are one byte they need to be swapped like they are 2
+        SimpleBuffer.flipByteOrderInPlace2(source.buffer)
     }
 
-    const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(intView)
-    const { divisor, summand } = header
-
-    const values = new Float32Array(header.xExtent * header.yExtent * header.zExtent)
-
-    let offset = 512
-    const xBlocks = Math.ceil(header.xExtent / 8)
-    const yBlocks = Math.ceil(header.yExtent / 8)
-    const zBlocks = Math.ceil(header.zExtent / 8)
+    const { divisor, summand, xExtent, yExtent, zExtent } = header
+    const { xBlocks, yBlocks, zBlocks } = getBlocks(header)
 
+    let offset = 0
     // loop over blocks
     for (let zz = 0; zz < zBlocks; ++zz) {
         for (let yy = 0; yy < yBlocks; ++yy) {
@@ -99,9 +99,9 @@ async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Res
                         for (let i = 0; i < 8; ++i) {
                             const x = 8 * xx + i
                             // check if remaining slice-part contains values
-                            if (x < header.xExtent && y < header.yExtent && z < header.zExtent) {
-                                const idx = ((((x * header.yExtent) + y) * header.zExtent) + z)
-                                values[ idx ] = (byteView[ offset ] - summand) / divisor
+                            if (x < xExtent && y < yExtent && z < zExtent) {
+                                const idx = ((((x * yExtent) + y) * zExtent) + z)
+                                target[idx] = (source[offset] - summand) / divisor
                                 ++offset
                             } else {
                                 offset += 8 - i
@@ -113,15 +113,41 @@ async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Res
             }
         }
     }
+}
+
+export function getDsn6Counts(header: Dsn6Header) {
+    const { xExtent, yExtent, zExtent } = header
+    const { xBlocks, yBlocks, zBlocks } = getBlocks(header)
+    const valueCount = xExtent * yExtent * zExtent
+    const count = xBlocks * 8 * yBlocks * 8 * zBlocks * 8
+    const elementByteSize = 1
+    const byteCount = count * elementByteSize
+    return { count, byteCount, valueCount }
+}
+
+async function parseInternal(file: FileHandle, size: number, ctx: RuntimeContext): Promise<Dsn6File> {
+    await ctx.update({ message: 'Parsing DSN6/BRIX file...' });
+    const { header, littleEndian } = await readDsn6Header(file)
+    const { buffer } = await file.readBuffer(dsn6HeaderSize, size - dsn6HeaderSize)
+    const { valueCount } = getDsn6Counts(header)
+
+    const values = new Float32Array(valueCount)
+    await parseDsn6Values(header, buffer, values, littleEndian)
 
     const result: Dsn6File = { header, values };
-    return Result.success(result);
+    return result;
 }
 
-export function parseFile(file: FileHandle) {
-    return Task.create<Result<Dsn6File>>('Parse DSN6/BRIX', ctx => parseInternal(file, ctx));
+export function parseFile(file: FileHandle, size: number) {
+    return Task.create<Result<Dsn6File>>('Parse DSN6/BRIX', async ctx => {
+        try {
+            return Result.success(await parseInternal(file, size, ctx));
+        } catch (e) {
+            return Result.error(e);
+        }
+    })
 }
 
 export function parse(buffer: Uint8Array) {
-    return parseFile(FileHandle.fromBuffer(buffer))
+    return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length)
 }

+ 1 - 1
src/mol-io/reader/dsn6/schema.ts

@@ -40,5 +40,5 @@ export interface Dsn6Header {
  */
 export interface Dsn6File {
     header: Dsn6Header
-    values: Float32Array | Int8Array
+    values: Float32Array
 }

+ 29 - 14
src/mol-model-formats/volume/ccp4.ts

@@ -8,12 +8,36 @@ import { VolumeData } from 'mol-model/volume/data'
 import { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
-import { Ccp4File } from 'mol-io/reader/ccp4/schema';
+import { Ccp4File, Ccp4Header } from 'mol-io/reader/ccp4/schema';
 import { degToRad } from 'mol-math/misc';
+import { getCcp4ValueType } from 'mol-io/reader/ccp4/parser';
+import { TypedArrayValueType } from 'mol-io/common/typed-array';
 
-function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
+/** When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START */
+export function getCcp4Origin(header: Ccp4Header) {
+    let gridOrigin: number[]
+    if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) {
+        gridOrigin = [header.NCSTART, header.NRSTART, header.NSSTART];
+    } else {
+        gridOrigin = [header.originX, header.originY, header.originZ];
+    }
+    return gridOrigin
+}
+
+function getTypedArrayCtor(header: Ccp4Header) {
+    const valueType = getCcp4ValueType(header)
+    switch (valueType) {
+        case TypedArrayValueType.Float32: return Float32Array;
+        case TypedArrayValueType.Int8: return Int8Array;
+        case TypedArrayValueType.Int16: return Int16Array;
+    }
+    throw Error(`${valueType} is not a supported value format.`);
+}
+
+export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> {
     return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { header, values } = source;
+        console.log({ header, values })
         const size = Vec3.create(header.xLength, header.yLength, header.zLength)
         if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize)
         const angles = Vec3.create(degToRad(header.alpha), degToRad(header.beta), degToRad(header.gamma))
@@ -24,19 +48,12 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V
 
         const grid = [header.NX, header.NY, header.NZ];
         const extent = normalizeOrder([header.NC, header.NR, header.NS]);
-
-        let gridOrigin: number[]
-        if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) {
-            gridOrigin = normalizeOrder([header.NCSTART, header.NRSTART, header.NSSTART]);
-        } else {
-            // When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START
-            gridOrigin = [header.originX, header.originY, header.originZ];
-        }
+        const gridOrigin = normalizeOrder(getCcp4Origin(header));
 
         const origin_frac = Vec3.create(gridOrigin[0] / grid[0], gridOrigin[1] / grid[1], gridOrigin[2] / grid[2]);
         const dimensions_frac = Vec3.create(extent[0] / grid[0], extent[1] / grid[1], extent[2] / grid[2]);
 
-        const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), header.MODE === 0 ? Int8Array : Float32Array);
+        const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), getTypedArrayCtor(header));
         const data = Tensor.create(space, Tensor.Data1(values));
 
         // TODO Calculate stats? When to trust header data?
@@ -55,6 +72,4 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V
             }
         };
     });
-}
-
-export { volumeFromCcp4 }
+}

+ 16 - 20
src/mol-model/volume/data.ts

@@ -21,10 +21,10 @@ interface VolumeData {
 }
 
 namespace VolumeData {
-    export const Empty: VolumeData = {
+    export const One: VolumeData = {
         cell: SpacegroupCell.Zero,
         fractionalBox: Box3D.empty(),
-        data: Tensor.create(Tensor.Space([0, 0, 0], [0, 1, 2]), Tensor.Data1([])),
+        data: Tensor.create(Tensor.Space([1, 1, 1], [0, 1, 2]), Tensor.Data1([0])),
         dataStats: { min: 0, max: 0, mean: 0, sigma: 0 }
     }
 
@@ -44,11 +44,11 @@ namespace VolumeData {
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative
 
 namespace VolumeIsoValue {
-    export type Relative = Readonly<{ kind: 'relative', stats: VolumeData['dataStats'], relativeValue: number }>
-    export type Absolute = Readonly<{ kind: 'absolute', stats: VolumeData['dataStats'], absoluteValue: number }>
+    export type Relative = Readonly<{ kind: 'relative', relativeValue: number }>
+    export type Absolute = Readonly<{ kind: 'absolute', absoluteValue: number }>
 
-    export function absolute(stats: VolumeData['dataStats'], value: number): Absolute { return { kind: 'absolute', stats, absoluteValue: value }; }
-    export function relative(stats: VolumeData['dataStats'], value: number): Relative { return { kind: 'relative', stats, relativeValue: value }; }
+    export function absolute(value: number): Absolute { return { kind: 'absolute', absoluteValue: value }; }
+    export function relative(value: number): Relative { return { kind: 'relative', relativeValue: value }; }
 
     export function calcAbsolute(stats: VolumeData['dataStats'], relativeValue: number): number {
         return relativeValue * stats.sigma + stats.mean
@@ -58,22 +58,18 @@ namespace VolumeIsoValue {
         return stats.sigma === 0 ? 0 : ((absoluteValue - stats.mean) / stats.sigma)
     }
 
-    export function toAbsolute(value: VolumeIsoValue): Absolute {
-        if (value.kind === 'absolute') return value;
-        return {
-            kind: 'absolute',
-            stats: value.stats,
-            absoluteValue: calcAbsolute(value.stats, value.relativeValue)
-        }
+    export function toAbsolute(value: VolumeIsoValue, stats: VolumeData['dataStats']): Absolute {
+        return value.kind === 'absolute' ? value : { kind: 'absolute', absoluteValue: VolumeIsoValue.calcAbsolute(stats, value.relativeValue) }
     }
 
-    export function toRelative(value: VolumeIsoValue): Relative {
-        if (value.kind === 'relative') return value;
-        return {
-            kind: 'relative',
-            stats: value.stats,
-            relativeValue: calcRelative(value.stats, value.absoluteValue)
-        }
+    export function toRelative(value: VolumeIsoValue, stats: VolumeData['dataStats']): Relative {
+        return value.kind === 'relative' ? value : { kind: 'relative', relativeValue: VolumeIsoValue.calcRelative(stats, value.absoluteValue) }
+    }
+
+    export function toString(value: VolumeIsoValue) {
+        return value.kind === 'relative'
+            ? `${value.relativeValue} σ`
+            : `${value.absoluteValue}`
     }
 }
 

+ 38 - 7
src/mol-plugin/behavior/behavior.ts

@@ -5,7 +5,7 @@
  */
 
 import { PluginStateTransform, PluginStateObject } from '../state/objects';
-import { Transformer, Transform } from 'mol-state';
+import { StateTransformer, StateTransform } from 'mol-state';
 import { Task } from 'mol-task';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommand } from '../command';
@@ -16,7 +16,7 @@ import { shallowEqual } from 'mol-util';
 export { PluginBehavior }
 
 interface PluginBehavior<P = unknown> {
-    register(ref: Transform.Ref): void,
+    register(ref: StateTransform.Ref): void,
     unregister(): void,
 
     /** Update params in place. Optionally return a promise if it depends on an async action. */
@@ -25,14 +25,24 @@ interface PluginBehavior<P = unknown> {
 
 namespace PluginBehavior {
     export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { }
+    export class Category extends PluginStateObject.Create({ name: 'Category', typeClass: 'Object' }) { }
     export class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { }
 
     export interface Ctor<P = undefined> { new(ctx: PluginContext, params: P): PluginBehavior<P> }
 
+    export const Categories = {
+        'common': 'Common',
+        'representation': 'Representation',
+        'interaction': 'Interaction',
+        'custom-props': 'Custom Properties',
+        'misc': 'Miscellaneous'
+    };
+
     export interface CreateParams<P> {
         name: string,
+        category: keyof typeof Categories,
         ctor: Ctor<P>,
-        canAutoUpdate?: Transformer.Definition<Root, Behavior, P>['canAutoUpdate'],
+        canAutoUpdate?: StateTransformer.Definition<Root, Behavior, P>['canAutoUpdate'],
         label?: (params: P) => { label: string, description?: string },
         display: {
             name: string,
@@ -42,9 +52,28 @@ namespace PluginBehavior {
         params?(a: Root, globalCtx: PluginContext): { [K in keyof P]: ParamDefinition.Any }
     }
 
+    export type CreateCategory = typeof CreateCategory
+    export const CreateCategory = PluginStateTransform.BuiltIn({
+        name: 'create-behavior-category',
+        display: { name: 'Behavior Category' },
+        from: Root,
+        to: Category,
+        params: {
+            label: ParamDefinition.Text('', { isHidden: true }),
+        }
+    })({
+        apply({ params }) {
+            return new Category({}, { label: params.label });
+        }
+    });
+
+    const categoryMap = new Map<string, string>();
+    export function getCategoryId(t: StateTransformer) {
+        return categoryMap.get(t.id)!;
+    }
+
     export function create<P>(params: CreateParams<P>) {
-        // TODO: cache groups etc
-        return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({
+        const t = PluginStateTransform.CreateBuiltIn<Category, Behavior, P>({
             name: params.name,
             display: params.display,
             from: [Root],
@@ -56,13 +85,15 @@ namespace PluginBehavior {
             },
             update({ b, newParams }) {
                 return Task.create('Update Behavior', async () => {
-                    if (!b.data.update) return Transformer.UpdateResult.Unchanged;
+                    if (!b.data.update) return StateTransformer.UpdateResult.Unchanged;
                     const updated = await b.data.update(newParams);
-                    return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged;
+                    return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
                 })
             },
             canAutoUpdate: params.canAutoUpdate
         });
+        categoryMap.set(t.id, params.category);
+        return t;
     }
 
     export function simpleCommandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) {

+ 2 - 2
src/mol-plugin/behavior/dynamic/animation.ts

@@ -10,8 +10,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { degToRad } from 'mol-math/misc';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
-import { StateSelection } from 'mol-state/state/selection';
-import { StateObjectCell, State } from 'mol-state';
+import { StateObjectCell, State, StateSelection } from 'mol-state';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
 import { UUID } from 'mol-util';
 
@@ -30,6 +29,7 @@ type StructureAnimationProps = PD.Values<typeof StructureAnimationParams>
  */
 export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({
     name: 'structure-animation',
+    category: 'representation',
     display: { name: 'Structure Animation', group: 'Animation' },
     canAutoUpdate: () => true,
     ctor: class extends PluginBehavior.Handler<StructureAnimationProps> {

+ 1 - 0
src/mol-plugin/behavior/dynamic/camera.ts

@@ -10,6 +10,7 @@ import { PluginBehavior } from '../behavior';
 
 export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({
     name: 'focus-loci-on-select',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> {
         register(): void {
             this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {

+ 1 - 0
src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts

@@ -16,6 +16,7 @@ import { ThemeDataContext } from 'mol-theme/theme';
 
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
+    category: 'custom-props',
     display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = StructureQualityReport.createAttachTask(

+ 1 - 0
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -16,6 +16,7 @@ import { Table } from 'mol-data/db';
 
 export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'rcsb-assembly-symmetry-prop',
+    category: 'custom-props',
     display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch);

+ 2 - 2
src/mol-plugin/behavior/dynamic/labels.ts

@@ -9,8 +9,7 @@ import { PluginBehavior } from '../behavior';
 import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
-import { StateSelection } from 'mol-state/state/selection';
-import { StateObjectCell, State } from 'mol-state';
+import { StateObjectCell, State, StateSelection } from 'mol-state';
 import { RuntimeContext } from 'mol-task';
 import { Shape } from 'mol-model/shape';
 import { Text } from 'mol-geo/geometry/text/text';
@@ -74,6 +73,7 @@ function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: T
 
 export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
     name: 'scene-labels',
+    category: 'representation',
     display: { name: 'Scene Labels', group: 'Labels' },
     canAutoUpdate: () => true,
     ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {

+ 4 - 2
src/mol-plugin/behavior/dynamic/representation.ts

@@ -10,14 +10,14 @@ import { EmptyLoci, Loci } from 'mol-model/loci';
 import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { StateObjectTracker } from 'mol-state';
-import { StateSelection } from 'mol-state/state/selection';
+import { StateObjectTracker, StateSelection } from 'mol-state';
 import { labelFirst } from 'mol-theme/label';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginBehavior } from '../behavior';
 
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
@@ -38,6 +38,7 @@ export const HighlightLoci = PluginBehavior.create({
 
 export const SelectLoci = PluginBehavior.create({
     name: 'representation-select-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
         register(): void {
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
@@ -60,6 +61,7 @@ export const SelectLoci = PluginBehavior.create({
 
 export const DefaultLociLabelProvider = PluginBehavior.create({
     name: 'default-loci-label-provider',
+    category: 'interaction',
     ctor: class implements PluginBehavior<undefined> {
         private f = labelFirst;
         register(): void { this.ctx.lociLabels.addProvider(this.f); }

+ 149 - 0
src/mol-plugin/behavior/dynamic/volume.ts

@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import CIF from 'mol-io/reader/cif';
+import { Box3D } from 'mol-math/geometry';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { createIsoValueParam } from 'mol-repr/volume/isosurface';
+import { Color } from 'mol-util/color';
+import { LRUCache } from 'mol-util/lru-cache';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginBehavior } from '../behavior';
+import { Structure } from 'mol-model/structure';
+
+export namespace VolumeStreaming {
+    function channelParam(label: string, color: Color, defaultValue: number) {
+        return PD.Group({
+            color: PD.Color(color),
+            isoValue: createIsoValueParam(VolumeIsoValue.relative(defaultValue))
+        }, { label });
+    }
+
+    export const Params = {
+        id: PD.Text('1tqn'),
+        levels: PD.MappedStatic('x-ray', {
+            'em': channelParam('EM', Color(0x638F8F), 1.5),
+            'x-ray': PD.Group({
+                '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), 1.5),
+                'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), 3),
+                'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), -3),
+            })
+        }),
+        box: PD.MappedStatic('static-box', {
+            'static-box': PD.Group({
+                bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)),
+                topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9))
+            }, { description: 'Static box defined by cartesian coords.' }),
+            // 'around-selection': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 10 }) }),
+            'cell': PD.Group({  }),
+            // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
+        }),
+        detailLevel: PD.Numeric(3, { min: 0, max: 7 }),
+        serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'),
+    }
+    export type Params = PD.Values<typeof Params>
+
+    export type ChannelData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
+    export type LevelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
+
+    export class Behavior implements PluginBehavior<Params> {
+        // TODO: have special value for "cell"?
+        private cache = LRUCache.create<ChannelData>(25);
+        // private ref: string = '';
+
+        currentData: ChannelData = { }
+
+        private async queryData(box?: Box3D) {
+            let url = `${this.params.serverUrl}/${this.params.levels.name}/${this.params.id}`
+
+            if (box) {
+                const { min: a, max: b } = box;
+                url += `/box`
+                    + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}`
+                    + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`;
+            } else {
+                url += `/cell`;
+            }
+            url += `?detail=${this.params.detailLevel}`;
+
+            let data = LRUCache.get(this.cache, url);
+            if (data) {
+                return data;
+            }
+
+            const cif = await this.ctx.runTask(this.ctx.fetch(url, 'binary'));
+            data = await this.parseCif(cif as Uint8Array);
+            if (!data) {
+                return;
+            }
+
+            LRUCache.set(this.cache, url, data);
+            return data;
+        }
+
+        private async parseCif(data: Uint8Array): Promise<ChannelData | undefined> {
+            const parsed = await this.ctx.runTask(CIF.parseBinary(data));
+            if (parsed.isError) {
+                this.ctx.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
+                return;
+            }
+            if (parsed.result.blocks.length < 2) {
+                this.ctx.log.error('VolumeStreaming: Invalid data.');
+                return;
+            }
+
+            const ret: ChannelData = { };
+            for (let i = 1; i < parsed.result.blocks.length; i++) {
+                const block = parsed.result.blocks[i];
+
+                const densityServerCif = CIF.schema.densityServer(block);
+                const volume = await this.ctx.runTask(await volumeFromDensityServerData(densityServerCif));
+                (ret as any)[block.header as any] = volume;
+            }
+            return ret;
+        }
+
+        register(ref: string): void {
+            // TODO: register camera movement/loci so that "around selection box works"
+            // alternatively, and maybe a better solution, write a global behavior that modifies this node from the outside
+        }
+
+        async update(params: Params): Promise<boolean> {
+            this.params = params;
+
+            let box: Box3D | undefined = void 0;
+
+            switch (params.box.name) {
+                case 'static-box':
+                    box = Box3D.create(params.box.params.bottomLeft, params.box.params.topRight);
+                    break;
+                case 'cell':
+                    box = this.params.levels.name === 'x-ray'
+                        ? this.structure.boundary.box
+                        : void 0;
+                    break;
+            }
+
+            const data = await this.queryData(box);
+            this.currentData = data || { };
+
+            return true;
+        }
+
+        unregister(): void {
+            // TODO unsubscribe to events
+        }
+
+        constructor(public ctx: PluginContext, public params: Params, private structure: Structure) {
+        }
+    }
+
+    export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Volume Streaming' }) { }
+}

+ 24 - 6
src/mol-plugin/behavior/static/state.ts

@@ -6,7 +6,7 @@
 
 import { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
-import { StateTree, Transform, State } from 'mol-state';
+import { StateTree, StateTransform, State } from 'mol-state';
 import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
 import { EmptyLoci, EveryLoci } from 'mol-model/loci';
@@ -51,7 +51,7 @@ export function SetCurrentObject(ctx: PluginContext) {
 }
 
 export function Update(ctx: PluginContext) {
-    PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.updateTree(tree)));
+    PluginCommands.State.Update.subscribe(ctx, ({ state, tree, doNotLogTiming }) => ctx.runTask(state.updateTree(tree, doNotLogTiming)));
 }
 
 export function ApplyAction(ctx: PluginContext) {
@@ -59,9 +59,27 @@ export function ApplyAction(ctx: PluginContext) {
 }
 
 export function RemoveObject(ctx: PluginContext) {
-    PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => {
-        const tree = state.tree.build().delete(ref).getTree();
+    function remove(state: State, ref: string) {
+        const tree = state.build().delete(ref).getTree();
         return ctx.runTask(state.updateTree(tree));
+    }
+
+    PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref, removeParentGhosts }) => {
+        if (removeParentGhosts) {
+            const tree = state.tree;
+            let curr = tree.transforms.get(ref);
+            if (curr.parent === ref) return remove(state, ref);
+
+            while (true) {
+                const children = tree.children.get(curr.parent);
+                if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
+                const parent = tree.transforms.get(curr.parent);
+                if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref);
+                curr = parent;
+            }
+        } else {
+            remove(state, ref);
+        }
     });
 }
 
@@ -73,11 +91,11 @@ export function ToggleVisibility(ctx: PluginContext) {
     PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
 }
 
-function setVisibility(state: State, root: Transform.Ref, value: boolean) {
+function setVisibility(state: State, root: StateTransform.Ref, value: boolean) {
     StateTree.doPreOrder(state.tree, state.transforms.get(root), { state, value }, setVisibilityVisitor);
 }
 
-function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State, value: boolean }) {
+function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state: State, value: boolean }) {
     ctx.state.updateCellState(t.ref, { isHidden: ctx.value });
 }
 

+ 9 - 10
src/mol-plugin/command.ts

@@ -6,8 +6,7 @@
 
 import { Camera } from 'mol-canvas3d/camera';
 import { PluginCommand } from './command/base';
-import { Transform, State } from 'mol-state';
-import { StateAction } from 'mol-state/action';
+import { StateTransform, State, StateAction } from 'mol-state';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginLayoutStateProps } from './layout';
 
@@ -15,16 +14,16 @@ export * from './command/base';
 
 export const PluginCommands = {
     State: {
-        SetCurrentObject: PluginCommand<{ state: State, ref: Transform.Ref }>(),
-        ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>(),
-        Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder }>(),
+        SetCurrentObject: PluginCommand<{ state: State, ref: StateTransform.Ref }>(),
+        ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: StateTransform.Ref }>(),
+        Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder, doNotLogTiming?: boolean }>(),
 
-        RemoveObject: PluginCommand<{ state: State, ref: Transform.Ref }>(),
+        RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(),
 
-        ToggleExpanded: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        ToggleVisibility: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        Highlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
-        ClearHighlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
+        ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
+        ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
+        Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
+        ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }),
 
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),

+ 19 - 21
src/mol-plugin/component.ts

@@ -4,39 +4,37 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { BehaviorSubject, Observable, Subject } from 'rxjs';
-import { PluginContext } from './context';
 import { shallowMergeArray } from 'mol-util/object';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
 
 export class PluginComponent<State> {
-    private _state: BehaviorSubject<State>;
-    private _updated = new Subject();
+    private _ev: RxEventHelper;
 
-    updateState(...states: Partial<State>[]) {
-        const latest = this.latestState;
-        const s = shallowMergeArray(latest, states);
-        if (s !== latest) {
-            this._state.next(s);
-        }
+    protected get ev() {
+        return this._ev || (this._ev = RxEventHelper.create());
     }
 
-    get states() {
-        return <Observable<State>>this._state;
-    }
+    private _state: State;
 
-    get latestState() {
-        return this._state.value;
+    protected updateState(...states: Partial<State>[]): boolean {
+        const latest = this.state;
+        const s = shallowMergeArray(latest, states);
+        if (s !== latest) {
+            this._state = s;
+            return true;
+        }
+        return false;
     }
 
-    get updated() {
-        return <Observable<{}>>this._updated;
+    get state() {
+        return this._state;
     }
 
-    triggerUpdate() {
-        this._updated.next({});
+    dispose() {
+        if (this._ev) this._ev.dispose();
     }
 
-    constructor(public context: PluginContext, initialState: State) {
-        this._state = new BehaviorSubject<State>(initialState);
+    constructor(initialState: State) {
+        this._state = initialState;
     }
 }

+ 22 - 8
src/mol-plugin/context.ts

@@ -8,7 +8,7 @@ import { Canvas3D } from 'mol-canvas3d/canvas3d';
 import { EmptyLoci, Loci } from 'mol-model/loci';
 import { Representation } from 'mol-repr/representation';
 import { StructureRepresentationRegistry } from 'mol-repr/structure/registry';
-import { State, Transform, Transformer } from 'mol-state';
+import { State, StateTransform, StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
 import { ColorTheme } from 'mol-theme/color';
 import { SizeTheme } from 'mol-theme/size';
@@ -30,7 +30,8 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { PluginLayout } from './layout';
 import { List } from 'immutable';
 import { StateTransformParameters } from './ui/state/common';
-import { DataFormatRegistry } from './state/actions/basic';
+import { DataFormatRegistry } from './state/actions/volume';
+import { PluginBehavior } from './behavior/behavior';
 
 export class PluginContext {
     private disposed = false;
@@ -98,7 +99,7 @@ export class PluginContext {
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
             this.layout.setRoot(container);
-            if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout);
+            if (this.spec.initialLayout) this.layout.setProps(this.spec.initialLayout);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             this.canvas3d.animate();
@@ -140,15 +141,16 @@ export class PluginContext {
         this.ev.dispose();
         this.state.dispose();
         this.tasks.dispose();
+        this.layout.dispose();
         this.disposed = true;
     }
 
-    applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) {
-        const tree = state.tree.build().to(a).apply(transformer, params);
+    applyTransform(state: State, a: StateTransform.Ref, transformer: StateTransformer, params: any) {
+        const tree = state.build().to(a).apply(transformer, params);
         return PluginCommands.State.Update.dispatch(this, { state, tree });
     }
 
-    updateTransform(state: State, a: Transform.Ref, params: any) {
+    updateTransform(state: State, a: StateTransform.Ref, params: any) {
         const tree = state.build().to(a).update(params);
         return PluginCommands.State.Update.dispatch(this, { state, tree });
     }
@@ -163,10 +165,14 @@ export class PluginContext {
     }
 
     private async initBehaviors() {
-        const tree = this.state.behaviorState.tree.build();
+        const tree = this.state.behaviorState.build();
+
+        for (const cat of Object.keys(PluginBehavior.Categories)) {
+            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } });
+        }
 
         for (const b of this.spec.behaviors) {
-            tree.toRoot().apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
+            tree.to(PluginBehavior.getCategoryId(b.transformer)).apply(b.transformer, b.defaultParams, { ref: b.transformer.id });
         }
 
         await this.runTask(this.state.behaviorState.updateTree(tree, true));
@@ -178,6 +184,13 @@ export class PluginContext {
         }
     }
 
+    private initAnimations() {
+        if (!this.spec.animations) return;
+        for (const anim of this.spec.animations) {
+            this.state.animation.register(anim);
+        }
+    }
+
     private initCustomParamEditors() {
         if (!this.spec.customParamEditors) return;
 
@@ -193,6 +206,7 @@ export class PluginContext {
 
         this.initBehaviors();
         this.initDataActions();
+        this.initAnimations();
         this.initCustomParamEditors();
 
         this.lociLabels = new LociLabelManager(this);

+ 15 - 7
src/mol-plugin/index.ts

@@ -11,9 +11,10 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { PluginCommands } from './command';
 import { PluginSpec } from './spec';
-import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
+import { AnimateModelIndex } from './state/animation/built-in';
+import { StateActions } from './state/actions';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -22,11 +23,15 @@ function getParam(name: string, regex: string): string {
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
-        PluginSpec.Action(DownloadStructure),
-        PluginSpec.Action(DownloadDensity),
-        PluginSpec.Action(OpenStructure),
-        PluginSpec.Action(OpenVolume),
-        PluginSpec.Action(CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.DownloadStructure),
+        PluginSpec.Action(StateActions.Volume.DownloadDensity),
+        PluginSpec.Action(StateActions.Structure.OpenStructure),
+        PluginSpec.Action(StateActions.Volume.OpenVolume),
+        PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation),
+        PluginSpec.Action(StateActions.Structure.EnableModelCustomProps),
+
+        PluginSpec.Action(StateActions.Volume.InitVolumeStreaming),
+
         PluginSpec.Action(StateTransforms.Data.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Data.ParseCcp4),
@@ -34,7 +39,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
-        PluginSpec.Action(StateTransforms.Model.VolumeFromCcp4),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),
         PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
@@ -48,6 +53,9 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
+    ],
+    animations: [
+        AnimateModelIndex
     ]
 }
 

+ 14 - 6
src/mol-plugin/layout.ts

@@ -42,21 +42,29 @@ interface RootState {
 }
 
 export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
+    readonly events = {
+        updated: this.ev()
+    }
+
     private updateProps(state: Partial<PluginLayoutStateProps>) {
-        let prevExpanded = !!this.latestState.isExpanded;
+        let prevExpanded = !!this.state.isExpanded;
         this.updateState(state);
         if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
 
-        this.triggerUpdate();
+        this.events.updated.next();
     }
 
     private root: HTMLElement;
     private rootState: RootState | undefined = void 0;
     private expandedViewport: HTMLMetaElement;
 
+    setProps(props: PluginLayoutStateProps) {
+        this.updateState(props);
+    }
+
     setRoot(root: HTMLElement) {
         this.root = root;
-        if (this.latestState.isExpanded) this.handleExpand();
+        if (this.state.isExpanded) this.handleExpand();
     }
 
     private getScrollElement() {
@@ -72,7 +80,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
 
             if (!body || !head) return;
 
-            if (this.latestState.isExpanded) {
+            if (this.state.isExpanded) {
                 let children = head.children;
                 let hasExp = false;
                 let viewports: HTMLElement[] = [];
@@ -167,8 +175,8 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
         }
     }
 
-    constructor(context: PluginContext) {
-        super(context, { ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout });
+    constructor(private context: PluginContext) {
+        super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout });
 
         PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
 

+ 0 - 1
src/mol-plugin/providers/custom-prop.ts

@@ -1 +0,0 @@
-// TODO

+ 0 - 1
src/mol-plugin/providers/theme.ts

@@ -1 +0,0 @@
-// TODO

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

@@ -76,19 +76,10 @@
     > div:first-child {
         position: absolute;
         top: 0;
-        left: 0;
+        left: 18px;
         bottom: 0;
-        right: 50px;
-        width: 100%;
-        padding-right: 50px;
-        display: table;
-        
-        > div {
-            height: $row-height;
-            display: table-cell;
-            vertical-align: middle;
-            padding: 0 ($control-spacing + 4px);
-        }
+        right: 62px;
+        display: grid;
     }
     > div:last-child {
         position: absolute;
@@ -101,9 +92,12 @@
         bottom: 0;
     }
     
-    // input[type=text] {
-    //     text-align: right;
-    // }
+    input[type=text] {
+        padding-right: 6px;
+        padding-left: 4px;
+        font-size: 80%;
+        text-align: right;
+    }
     
     // input[type=range] {
     //     width: 100%;
@@ -125,20 +119,10 @@
     > div:nth-child(2) {
         position: absolute;
         top: 0;
-        left: 0;
+        left: 35px;
         bottom: 0;
-        right: 25px;
-        width: 100%;
-        padding-left: 20px;
-        padding-right: 25px;
-        display: table;
-        
-        > div {
-            height: $row-height;
-            display: table-cell;
-            vertical-align: middle;
-            padding: 0 ($control-spacing + 4px);
-        }
+        right: 37px;
+        display: grid;
     }
     > div:last-child {
         position: absolute;
@@ -152,9 +136,12 @@
         font-size: 80%;
     }
     
-    // input[type=text] {
-    //     text-align: right;
-    // }
+    input[type=text] {
+        padding-right: 4px;
+        padding-left: 4px;
+        font-size: 80%;
+        text-align: center;
+    }
     
     // input[type=range] {
     //     width: 100%;

+ 4 - 0
src/mol-plugin/skin/base/components/misc.scss

@@ -66,4 +66,8 @@
     background: white;
     cursor: inherit;
     display: block;
+}
+
+.msp-animation-section {
+    margin-bottom: $control-spacing;
 }

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

@@ -14,6 +14,7 @@
   padding: 5px 0;
   width: 100%;
   border-radius: $slider-border-radius-base;
+  align-self: center;
   @include borderBox;
 
   &-rail {

+ 34 - 2
src/mol-plugin/skin/base/components/temp.scss

@@ -14,6 +14,15 @@
     // border-bottom: 1px solid $entity-color-Group; // TODO separate color
 }
 
+.msp-current-header {
+    height: $row-height;
+    line-height: $row-height;
+    margin-bottom: $control-spacing;
+    text-align: center;
+    font-weight: bold;
+    background: $default-background;
+}
+
 .msp-btn-row-group {
     display:flex;
     flex-direction:row;
@@ -69,10 +78,32 @@
     margin-bottom: 1px;
     padding-left: $row-height;
     padding-right: 2 * $row-height + $control-spacing;
-    border-bottom-left-radius: $control-spacing;
+    border-left: 1px solid $entity-color-Group; // TODO custom color
+    // border-bottom-left-radius: $control-spacing;
 
     &-current {
-        background: $control-background
+        // background: $control-background;
+        
+        a {
+            color: $font-color;
+        }
+
+        a:hover, a:hover > small {
+            color: color-lower-contrast($font-color, 24%);
+        }
+    }
+
+    a {
+        display: block;
+    }
+
+    a > small {
+        color: $font-color;
+    }
+
+    a:hover {
+        font-weight: bold;
+        text-decoration: none;
     }
 }
 
@@ -90,6 +121,7 @@
     left: 0;
     top: 0;
     width: $row-height;
+    padding: 0;
     color: color-lower-contrast($font-color, 24%);
 }
 

+ 41 - 8
src/mol-plugin/skin/base/components/transformer.scss

@@ -10,7 +10,7 @@
     }
 }
 
-.msp-layout-right {
+.msp-layout-right, .msp-layout-left {
     background: $control-background;
 }
 
@@ -42,14 +42,44 @@
     margin-bottom: $control-spacing;
 }
 
+.msp-transform-update-wrapper {
+    margin-bottom: 1px;
+}
+
+.msp-transform-update-wrapper-collapsed {
+    margin-bottom: 1px;
+}
+
+.msp-transform-update-wrapper, .msp-transform-update-wrapper-collapsed {
+    > .msp-transform-header > button {
+        text-align: left;
+        padding-left: $row-height;
+        line-height: 24px;
+        background: color-lower-contrast($control-background, 4%); // $control-background; // color-lower-contrast($default-background, 4%);
+        // font-weight: bold;
+    }
+}
+
+.msp-transform-wrapper > .msp-transform-header > button {
+    text-align: left;
+    background: color-lower-contrast($default-background, 4%);
+    font-weight: bold;
+}
+
 .msp-transform-header {
     position: relative;
-    border-top: 1px solid $entity-color-Behaviour; // TODO: separate color
+    // border-top: 1px solid $entity-color-Behaviour; // TODO: separate color
 
-    > button {
-        text-align: left;
-        background: color-lower-contrast($default-background, 4%);
-        font-weight: bold;
+    // > button {
+    //     text-align: left;
+    //     padding-left: $row-height;
+    //     background: $control-background; // color-lower-contrast($default-background, 4%);
+    //     font-weight: bold;
+    // }
+
+    > button > small {
+        font-weight: normal;
+        float: right;
     }
 
     > button:hover {
@@ -58,9 +88,11 @@
 }
 
 .msp-transform-default-params {
+    background: $default-background;
     position: absolute;
-    right: 0;
+    left: 0;
     top: 0;
+    width: $row-height;
 }
 
 .msp-transform-default-params:hover {
@@ -74,7 +106,8 @@
 }
 
 .msp-transform-refresh {
-    width: $control-label-width + $control-spacing;
+    width: $control-label-width + $control-spacing - $row-height - 1;
+    margin-left: $row-height + 1;
     background: $default-background;
     text-align: right;
 }

+ 8 - 7
src/mol-plugin/spec.ts

@@ -4,37 +4,38 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateAction } from 'mol-state/action';
-import { Transformer } from 'mol-state';
+import { StateTransformer, StateAction } from 'mol-state';
 import { StateTransformParameters } from './ui/state/common';
 import { PluginLayoutStateProps } from './layout';
+import { PluginStateAnimation } from './state/animation/model';
 
 export { PluginSpec }
 
 interface PluginSpec {
     actions: PluginSpec.Action[],
     behaviors: PluginSpec.Behavior[],
-    customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][]
+    animations?: PluginStateAnimation[],
+    customParamEditors?: [StateAction | StateTransformer, StateTransformParameters.Class][],
     initialLayout?: PluginLayoutStateProps
 }
 
 namespace PluginSpec {
     export interface Action {
-        action: StateAction | Transformer,
+        action: StateAction | StateTransformer,
         customControl?: StateTransformParameters.Class,
         autoUpdate?: boolean
     }
 
-    export function Action(action: StateAction | Transformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action {
+    export function Action(action: StateAction | StateTransformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action {
         return { action, customControl: params && params.customControl, autoUpdate: params && params.autoUpdate };
     }
 
     export interface Behavior {
-        transformer: Transformer,
+        transformer: StateTransformer,
         defaultParams?: any
     }
 
-    export function Behavior<T extends Transformer>(transformer: T, defaultParams?: Transformer.Params<T>): Behavior {
+    export function Behavior<T extends StateTransformer>(transformer: T, defaultParams?: StateTransformer.Params<T>): Behavior {
         return { transformer, defaultParams };
     }
 }

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

@@ -13,6 +13,7 @@ import { PluginStateSnapshotManager } from './state/snapshots';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginCommands } from './command';
+import { PluginAnimationManager } from './state/animation/manager';
 export { PluginState }
 
 class PluginState {
@@ -20,8 +21,8 @@ class PluginState {
 
     readonly dataState: State;
     readonly behaviorState: State;
+    readonly animation: PluginAnimationManager;
     readonly cameraSnapshots = new CameraSnapshotManager();
-
     readonly snapshots = new PluginStateSnapshotManager();
 
     readonly behavior = {
@@ -43,6 +44,7 @@ class PluginState {
         return {
             data: this.dataState.getSnapshot(),
             behaviour: this.behaviorState.getSnapshot(),
+            animation: this.animation.getSnapshot(),
             cameraSnapshots: this.cameraSnapshots.getStateSnapshot(),
             canvas3d: {
                 camera: this.plugin.canvas3d.camera.getSnapshot(),
@@ -60,6 +62,9 @@ class PluginState {
             if (snapshot.canvas3d.camera) this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
         }
         this.plugin.canvas3d.requestDraw(true);
+        if (snapshot.animation) {
+            this.animation.setSnapshot(snapshot.animation);
+        }
     }
 
     dispose() {
@@ -67,11 +72,12 @@ class PluginState {
         this.dataState.dispose();
         this.behaviorState.dispose();
         this.cameraSnapshots.dispose();
+        this.animation.dispose();
     }
 
     constructor(private plugin: import('./context').PluginContext) {
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
 
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o);
@@ -81,6 +87,8 @@ class PluginState {
         });
 
         this.behavior.currentObject.next(this.dataState.behaviors.currentObject.value);
+
+        this.animation = new PluginAnimationManager(plugin);
     }
 }
 
@@ -90,6 +98,7 @@ namespace PluginState {
     export interface Snapshot {
         data?: State.Snapshot,
         behaviour?: State.Snapshot,
+        animation?: PluginAnimationManager.Snapshot,
         cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
         canvas3d?: {
             camera?: Camera.Snapshot,

+ 13 - 0
src/mol-plugin/state/actions.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as Structure from './actions/structure'
+import * as Volume from './actions/volume'
+
+export const StateActions = {
+    Structure,
+    Volume
+}

+ 0 - 391
src/mol-plugin/state/actions/basic.ts

@@ -1,391 +0,0 @@
-/**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { PluginContext } from 'mol-plugin/context';
-import { StateTree, Transformer, StateObject } from 'mol-state';
-import { StateAction } from 'mol-state/action';
-import { StateSelection } from 'mol-state/state/selection';
-import { StateTreeBuilder } from 'mol-state/tree/builder';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { PluginStateObject } from '../objects';
-import { StateTransforms } from '../transforms';
-import { Download } from '../transforms/data';
-import { StructureRepresentation3DHelpers } from '../transforms/representation';
-import { getFileInfo, FileInfo } from 'mol-util/file-info';
-import { Task } from 'mol-task';
-
-// TODO: "structure/volume parser provider"
-
-export { DownloadStructure };
-type DownloadStructure = typeof DownloadStructure
-const DownloadStructure = StateAction.build({
-    from: PluginStateObject.Root,
-    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
-    params: {
-        source: PD.MappedStatic('bcif-static', {
-            'pdbe-updated': PD.Group({
-                id: PD.Text('1cbs', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'rcsb': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'bcif-static': PD.Group({
-                id: PD.Text('1tqn', { label: 'Id' }),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true }),
-            'url': PD.Group({
-                url: PD.Text(''),
-                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
-                isBinary: PD.Boolean(false),
-                supportProps: PD.Boolean(false)
-            }, { isFlat: true })
-        }, {
-            options: [
-                ['pdbe-updated', 'PDBe Updated'],
-                ['rcsb', 'RCSB'],
-                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
-                ['url', 'URL']
-            ]
-        })
-    }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const src = params.source;
-    let downloadParams: Transformer.Params<Download>;
-
-    switch (src.name) {
-        case 'url':
-            downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
-            break;
-        case 'pdbe-updated':
-            downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
-            break;
-        case 'rcsb':
-            downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
-            break;
-        case 'bcif-static':
-            downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
-            break;
-        default: throw new Error(`${(src as any).name} not supported.`);
-    }
-
-    const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams);
-    const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
-    return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
-});
-
-export const OpenStructure = StateAction.build({
-    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
-    from: PluginStateObject.Root,
-    params: { file: PD.File({ accept: '.cif,.bcif' }) }
-})(({ params, state }, ctx: PluginContext) => {
-    const b = state.build();
-    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
-    const traj = createModelTree(data, 'cif');
-    return state.updateTree(createStructureTree(ctx, traj, false));
-});
-
-function createModelTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
-    const parsed = format === 'cif'
-        ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
-        : b.apply(StateTransforms.Model.TrajectoryFromPDB);
-
-    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
-}
-
-function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree {
-    let root = b;
-    if (supportProps) {
-        root = root.apply(StateTransforms.Model.CustomModelProperties);
-    }
-    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
-    complexRepresentation(ctx, structure);
-
-    return root.getTree();
-}
-
-function complexRepresentation(ctx: PluginContext, root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) {
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
-    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
-        .apply(StateTransforms.Representation.StructureRepresentation3D,
-            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
-}
-
-export const CreateComplexRepresentation = StateAction.build({
-    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
-    from: PluginStateObject.Molecule.Structure
-})(({ ref, state }, ctx: PluginContext) => {
-    const root = state.build().to(ref);
-    complexRepresentation(ctx, root);
-    return state.updateTree(root.getTree());
-});
-
-export const UpdateTrajectory = StateAction.build({
-    display: { name: 'Update Trajectory' },
-    params: {
-        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
-        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
-    }
-})(({ params, state }) => {
-    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
-
-    const update = state.build();
-
-    if (params.action === 'reset') {
-        for (const m of models) {
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                () => ({ modelIndex: 0 }));
-        }
-    } else {
-        for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
-            if (!parent || !parent.obj) continue;
-            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                old => {
-                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                    if (modelIndex < 0) modelIndex += traj.data.length;
-                    return { modelIndex };
-                });
-        }
-    }
-
-    return state.updateTree(update);
-});
-
-//
-
-export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> {
-    private _list: { name: string, provider: DataFormatProvider<D, M> }[] = []
-    private _map = new Map<string, DataFormatProvider<D, M>>()
-
-    get default() { return this._list[0]; }
-    get types(): [string, string][] {
-        return this._list.map(e => [e.name, e.provider.label] as [string, string]);
-    }
-
-    constructor() {
-        this.add('ccp4', Ccp4Provider)
-        this.add('dsn6', Dsn6Provider)
-        this.add('dscif', DscifProvider)
-    };
-
-    add(name: string, provider: DataFormatProvider<D, M>) {
-        this._list.push({ name, provider })
-        this._map.set(name, provider)
-    }
-
-    remove(name: string) {
-        this._list.splice(this._list.findIndex(e => e.name === name), 1)
-        this._map.delete(name)
-    }
-
-    auto(info: FileInfo, dataStateObject: D) {
-        for (let i = 0, il = this.list.length; i < il; ++i) {
-            const { provider } = this._list[i]
-            if (provider.isApplicable(info, dataStateObject.data)) return provider
-        }
-        throw new Error('no compatible data format provider available')
-    }
-
-    get(name: string): DataFormatProvider<D, M> {
-        if (this._map.has(name)) {
-            return this._map.get(name)!
-        } else {
-            throw new Error(`unknown data format name '${name}'`)
-        }
-    }
-
-    get list() {
-        return this._list
-    }
-}
-
-interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> {
-    label: string
-    description: string
-    fileExtensions: string[]
-    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
-    getDefaultBuilder(b: StateTreeBuilder.To<D>): StateTreeBuilder.To<M>
-}
-
-const Ccp4Provider: DataFormatProvider<any, any> = {
-    label: 'CCP4/MRC/BRIX',
-    description: 'CCP4/MRC/BRIX',
-    fileExtensions: ['ccp4', 'mrc', 'map'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map'
-    },
-    getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => {
-        return b.apply(StateTransforms.Data.ParseCcp4)
-            .apply(StateTransforms.Model.VolumeFromCcp4)
-            .apply(StateTransforms.Representation.VolumeRepresentation3D)
-    }
-}
-
-const Dsn6Provider: DataFormatProvider<any, any> = {
-    label: 'DSN6/BRIX',
-    description: 'DSN6/BRIX',
-    fileExtensions: ['dsn6', 'brix'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'dsn6' || info.ext === 'brix'
-    },
-    getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => {
-        return b.apply(StateTransforms.Data.ParseDsn6)
-            .apply(StateTransforms.Model.VolumeFromDsn6)
-            .apply(StateTransforms.Representation.VolumeRepresentation3D)
-    }
-}
-
-const DscifProvider: DataFormatProvider<any, any> = {
-    label: 'DensityServer CIF',
-    description: 'DensityServer CIF',
-    fileExtensions: ['cif'],
-    isApplicable: (info: FileInfo, data: Uint8Array) => {
-        return info.ext === 'cif'
-    },
-    getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => {
-        return b.apply(StateTransforms.Data.ParseCif, {  })
-            .apply(StateTransforms.Model.VolumeFromDensityServerCif)
-            .apply(StateTransforms.Representation.VolumeRepresentation3D)
-    }
-}
-
-//
-
-function getDataFormatExtensionsOptions(dataFormatRegistry: DataFormatRegistry<any, any>) {
-    const extensions: string[] = []
-    const options: [string, string][] = [['auto', 'Automatic']]
-    dataFormatRegistry.list.forEach(({ name, provider }) => {
-        extensions.push(...provider.fileExtensions)
-        options.push([ name, provider.label ])
-    })
-    return { extensions, options }
-}
-
-export const OpenVolume = StateAction.build({
-    display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' },
-    from: PluginStateObject.Root,
-    params: (a, ctx: PluginContext) => {
-        const { extensions, options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
-        return {
-            file: PD.File({ accept: extensions.map(e => `.${e}`).join(',')}),
-            format: PD.Select('auto', options),
-            isBinary: PD.Boolean(true), // TOOD should take selected format into account
-        }
-    }
-})(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => {
-    const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary });
-    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
-
-    // Alternative for more complex states where the builder is not a simple StateTreeBuilder.To<>:
-    /*
-    const dataRef = dataTree.ref;
-    await state.updateTree(dataTree).runInContext(taskCtx);
-    const dataCell = state.select(dataRef)[0];
-    */
-
-    const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(params.file), dataStateObject) : ctx.dataFormat.registry.get(params.format)
-    const b = state.build().to(data.ref);
-    const tree = provider.getDefaultBuilder(b).getTree()
-    // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
-    await state.updateTree(tree).runInContext(taskCtx);
-}));
-
-export { DownloadDensity };
-type DownloadDensity = typeof DownloadDensity
-const DownloadDensity = StateAction.build({
-    from: PluginStateObject.Root,
-    display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
-    params: (a, ctx: PluginContext) => {
-        const { options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
-        return {
-            source: PD.MappedStatic('rcsb', {
-                'pdbe': PD.Group({
-                    id: PD.Text('1tqn', { label: 'Id' }),
-                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
-                }, { isFlat: true }),
-                'rcsb': PD.Group({
-                    id: PD.Text('1tqn', { label: 'Id' }),
-                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
-                }, { isFlat: true }),
-                'url': PD.Group({
-                    url: PD.Text(''),
-                    isBinary: PD.Boolean(false),
-                    format: PD.Select('auto', options),
-                }, { isFlat: true })
-            }, {
-                options: [
-                    ['pdbe', 'PDBe X-ray maps'],
-                    ['rcsb', 'RCSB X-ray maps'],
-                    ['url', 'URL']
-                ]
-            })
-        }
-    }
-})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => {
-    const src = params.source;
-    let downloadParams: Transformer.Params<Download>;
-    let provider: DataFormatProvider<any, any>
-
-    switch (src.name) {
-        case 'url':
-            downloadParams = src.params;
-            break;
-        case 'pdbe':
-            downloadParams = {
-                url: src.params.type === '2fofc'
-                    ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4`
-                    : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`,
-                isBinary: true,
-                label: `PDBe X-ray map: ${src.params.id}`
-            };
-            break;
-        case 'rcsb':
-            downloadParams = {
-                url: src.params.type === '2fofc'
-                    ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6`
-                    : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`,
-                isBinary: true,
-                label: `RCSB X-ray map: ${src.params.id}`
-            };
-            break;
-        default: throw new Error(`${(src as any).name} not supported.`);
-    }
-
-    const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams);
-    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
-
-    switch (src.name) {
-        case 'url':
-            downloadParams = src.params;
-            provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format)
-            break;
-        case 'pdbe':
-            provider = ctx.dataFormat.registry.get('ccp4')
-            break;
-        case 'rcsb':
-            provider = ctx.dataFormat.registry.get('dsn6')
-            break;
-        default: throw new Error(`${(src as any).name} not supported.`);
-    }
-
-    const b = state.build().to(data.ref);
-    const tree = provider.getDefaultBuilder(b).getTree()
-    await state.updateTree(tree).runInContext(taskCtx);
-}));

+ 180 - 0
src/mol-plugin/state/actions/structure.ts

@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginContext } from 'mol-plugin/context';
+import { StateAction, StateBuilder, StateSelection, StateTransformer } from 'mol-state';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { Download } from '../transforms/data';
+import { StructureRepresentation3DHelpers } from '../transforms/representation';
+import { CustomModelProperties } from '../transforms/model';
+
+// TODO: "structure/volume parser provider"
+
+export { DownloadStructure };
+type DownloadStructure = typeof DownloadStructure
+const DownloadStructure = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
+    params: {
+        source: PD.MappedStatic('bcif-static', {
+            'pdbe-updated': PD.Group({
+                id: PD.Text('1cbs', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'rcsb': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'bcif-static': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'url': PD.Group({
+                url: PD.Text(''),
+                format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]),
+                isBinary: PD.Boolean(false),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true })
+        }, {
+            options: [
+                ['pdbe-updated', 'PDBe Updated'],
+                ['rcsb', 'RCSB'],
+                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
+                ['url', 'URL']
+            ]
+        })
+    }
+})(({ params, state }, ctx: PluginContext) => {
+    const b = state.build();
+    const src = params.source;
+    let downloadParams: StateTransformer.Params<Download>;
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = { url: src.params.url, isBinary: src.params.isBinary };
+            break;
+        case 'pdbe-updated':
+            downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
+            break;
+        case 'rcsb':
+            downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
+            break;
+        case 'bcif-static':
+            downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams, { props: { isGhost: true }});
+    const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps));
+});
+
+export const OpenStructure = StateAction.build({
+    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
+    from: PluginStateObject.Root,
+    params: { file: PD.File({ accept: '.cif,.bcif' }) }
+})(({ params, state }, ctx: PluginContext) => {
+    const b = state.build();
+    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
+    const traj = createModelTree(data, 'cif');
+    return state.updateTree(createStructureTree(ctx, traj, false));
+});
+
+function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') {
+    const parsed = format === 'cif'
+        ? b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true }}).apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true }})
+        : b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true }});
+
+    return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+}
+
+function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean) {
+    let root = b;
+    if (supportProps) {
+        root = root.apply(StateTransforms.Model.CustomModelProperties);
+    }
+    const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
+    complexRepresentation(ctx, structure);
+
+    return root;
+}
+
+function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) {
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon'));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick'));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 }));
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D,
+            StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill'));
+}
+
+export const CreateComplexRepresentation = StateAction.build({
+    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
+    from: PluginStateObject.Molecule.Structure
+})(({ ref, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref);
+    complexRepresentation(ctx, root);
+    return state.updateTree(root);
+});
+
+export const UpdateTrajectory = StateAction.build({
+    display: { name: 'Update Trajectory' },
+    params: {
+        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
+        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
+    }
+})(({ params, state }) => {
+    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+
+    const update = state.build();
+
+    if (params.action === 'reset') {
+        for (const m of models) {
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                () => ({ modelIndex: 0 }));
+        }
+    } else {
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                old => {
+                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                    if (modelIndex < 0) modelIndex += traj.data.length;
+                    return { modelIndex };
+                });
+        }
+    }
+
+    return state.updateTree(update);
+});
+
+export const EnableModelCustomProps = StateAction.build({
+    display: { name: 'Custom Properties', description: 'Enable the addition of custom properties to the model.' },
+    from: PluginStateObject.Molecule.Model,
+    params(a, ctx: PluginContext) {
+        if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) };
+        return { properties: ctx.customModelProperties.getSelect(a.data) };
+    },
+    isApplicable(a, t, ctx: PluginContext) {
+        return t.transformer !== CustomModelProperties;
+    }
+})(({ ref, params, state }, ctx: PluginContext) => {
+    const root = state.build().to(ref).insert(CustomModelProperties, params);
+    return state.updateTree(root);
+});

+ 314 - 0
src/mol-plugin/state/actions/volume.ts

@@ -0,0 +1,314 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { VolumeIsoValue } from 'mol-model/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { State, StateAction, StateBuilder, StateObject, StateTransformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { ColorNames } from 'mol-util/color/tables';
+import { FileInfo, getFileInfo } from 'mol-util/file-info';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { Download } from '../transforms/data';
+import { VolumeRepresentation3DHelpers } from '../transforms/representation';
+import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume';
+
+export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> {
+    private _list: { name: string, provider: DataFormatProvider<D> }[] = []
+    private _map = new Map<string, DataFormatProvider<D>>()
+
+    get default() { return this._list[0]; }
+    get types(): [string, string][] {
+        return this._list.map(e => [e.name, e.provider.label] as [string, string]);
+    }
+
+    constructor() {
+        this.add('ccp4', Ccp4Provider)
+        this.add('dsn6', Dsn6Provider)
+        this.add('dscif', DscifProvider)
+    };
+
+    add(name: string, provider: DataFormatProvider<D>) {
+        this._list.push({ name, provider })
+        this._map.set(name, provider)
+    }
+
+    remove(name: string) {
+        this._list.splice(this._list.findIndex(e => e.name === name), 1)
+        this._map.delete(name)
+    }
+
+    auto(info: FileInfo, dataStateObject: D) {
+        for (let i = 0, il = this.list.length; i < il; ++i) {
+            const { provider } = this._list[i]
+            if (provider.isApplicable(info, dataStateObject.data)) return provider
+        }
+        throw new Error('no compatible data format provider available')
+    }
+
+    get(name: string): DataFormatProvider<D> {
+        if (this._map.has(name)) {
+            return this._map.get(name)!
+        } else {
+            throw new Error(`unknown data format name '${name}'`)
+        }
+    }
+
+    get list() {
+        return this._list
+    }
+}
+
+interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> {
+    label: string
+    description: string
+    fileExtensions: string[]
+    isApplicable(info: FileInfo, data: string | Uint8Array): boolean
+    getDefaultBuilder(ctx: PluginContext, data: StateBuilder.To<D>, state?: State): Task<void>
+}
+
+const Ccp4Provider: DataFormatProvider<any> = {
+    label: 'CCP4/MRC/BRIX',
+    description: 'CCP4/MRC/BRIX',
+    fileExtensions: ['ccp4', 'mrc', 'map'],
+    isApplicable: (info: FileInfo, data: Uint8Array) => {
+        return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+        return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParseCcp4)
+                .apply(StateTransforms.Volume.VolumeFromCcp4)
+                .apply(StateTransforms.Representation.VolumeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
+
+const Dsn6Provider: DataFormatProvider<any> = {
+    label: 'DSN6/BRIX',
+    description: 'DSN6/BRIX',
+    fileExtensions: ['dsn6', 'brix'],
+    isApplicable: (info: FileInfo, data: Uint8Array) => {
+        return info.ext === 'dsn6' || info.ext === 'brix'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+        return Task.create('DSN6/BRIX default builder', async taskCtx => {
+            const tree = data.apply(StateTransforms.Data.ParseDsn6)
+                .apply(StateTransforms.Volume.VolumeFromDsn6)
+                .apply(StateTransforms.Representation.VolumeRepresentation3D)
+            await state.updateTree(tree).runInContext(taskCtx)
+        })
+    }
+}
+
+const DscifProvider: DataFormatProvider<any> = {
+    label: 'DensityServer CIF',
+    description: 'DensityServer CIF',
+    fileExtensions: ['cif'],
+    isApplicable: (info: FileInfo, data: Uint8Array) => {
+        return info.ext === 'cif'
+    },
+    getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => {
+        return Task.create('DensityServer CIF default builder', async taskCtx => {
+            const cifBuilder = data.apply(StateTransforms.Data.ParseCif)
+            const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx)
+            const b = state.build().to(cifBuilder.ref);
+            const blocks = cifStateObject.data.blocks.slice(1); // zero block contains query meta-data
+            let tree: StateBuilder.To<any>
+            if (blocks.length === 1) {
+                tree = b
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal }))
+            } else if (blocks.length === 2) {
+                tree = b
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header })
+                    .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue }))
+                const vol = tree.to(cifBuilder.ref)
+                    .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[1].header })
+                const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green })
+                tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams)
+                const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red })
+                tree = tree.to(vol.ref).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams)
+            } else {
+                throw new Error('unknown number of blocks')
+            }
+
+            await state.updateTree(tree).runInContext(taskCtx);
+        })
+    }
+}
+
+//
+
+function getDataFormatExtensionsOptions(dataFormatRegistry: DataFormatRegistry<any, any>) {
+    const extensions: string[] = []
+    const options: [string, string][] = [['auto', 'Automatic']]
+    dataFormatRegistry.list.forEach(({ name, provider }) => {
+        extensions.push(...provider.fileExtensions)
+        options.push([ name, provider.label ])
+    })
+    return { extensions, options }
+}
+
+export const OpenVolume = StateAction.build({
+    display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' },
+    from: PluginStateObject.Root,
+    params: (a, ctx: PluginContext) => {
+        const { extensions, options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
+        return {
+            file: PD.File({ accept: extensions.map(e => `.${e}`).join(',')}),
+            format: PD.Select('auto', options),
+            isBinary: PD.Boolean(true), // TOOD should take selected format into account
+        }
+    }
+})(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => {
+    const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary });
+    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
+
+    // Alternative for more complex states where the builder is not a simple StateBuilder.To<>:
+    /*
+    const dataRef = dataTree.ref;
+    await state.updateTree(dataTree).runInContext(taskCtx);
+    const dataCell = state.select(dataRef)[0];
+    */
+
+    const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(params.file), dataStateObject) : ctx.dataFormat.registry.get(params.format)
+    const b = state.build().to(data.ref);
+    // need to await the 2nd update the so that the enclosing Task finishes after the update is done.
+    await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
+}));
+
+export { DownloadDensity };
+type DownloadDensity = typeof DownloadDensity
+const DownloadDensity = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' },
+    params: (a, ctx: PluginContext) => {
+        const { options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry)
+        return {
+            source: PD.MappedStatic('rcsb', {
+                'pdbe': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
+                }, { isFlat: true }),
+                'pdbe-emd-ds': PD.Group({
+                    id: PD.Text('emd-8004', { label: 'Id' }),
+                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                }, { isFlat: true }),
+                'pdbe-xray-ds': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }),
+                }, { isFlat: true }),
+                'rcsb': PD.Group({
+                    id: PD.Text('1tqn', { label: 'Id' }),
+                    type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
+                }, { isFlat: true }),
+                'url': PD.Group({
+                    url: PD.Text(''),
+                    isBinary: PD.Boolean(false),
+                    format: PD.Select('auto', options),
+                }, { isFlat: true })
+            }, {
+                options: [
+                    ['pdbe', 'PDBe X-ray maps'],
+                    ['pdbe-emd-ds', 'PDBe EMD Density Server'],
+                    ['pdbe-xray-ds', 'PDBe X-ray Density Server'],
+                    ['rcsb', 'RCSB X-ray maps'],
+                    ['url', 'URL']
+                ]
+            })
+        }
+    }
+})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => {
+    const src = params.source;
+    let downloadParams: StateTransformer.Params<Download>;
+    let provider: DataFormatProvider<any>
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = src.params;
+            break;
+        case 'pdbe':
+            downloadParams = {
+                url: src.params.type === '2fofc'
+                    ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4`
+                    : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`,
+                isBinary: true,
+                label: `PDBe X-ray map: ${src.params.id}`
+            };
+            break;
+        case 'pdbe-emd-ds':
+            downloadParams = {
+                url: `https://www.ebi.ac.uk/pdbe/densities/emd/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
+                isBinary: true,
+                label: `PDBe EMD Density Server: ${src.params.id}`
+            };
+            break;
+        case 'pdbe-xray-ds':
+            downloadParams = {
+                url: `https://www.ebi.ac.uk/pdbe/densities/x-ray/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`,
+                isBinary: true,
+                label: `PDBe X-ray Density Server: ${src.params.id}`
+            };
+            break;
+        case 'rcsb':
+            downloadParams = {
+                url: src.params.type === '2fofc'
+                    ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6`
+                    : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`,
+                isBinary: true,
+                label: `RCSB X-ray map: ${src.params.id}`
+            };
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams);
+    const dataStateObject = await state.updateTree(data).runInContext(taskCtx);
+
+    switch (src.name) {
+        case 'url':
+            downloadParams = src.params;
+            provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format)
+            break;
+        case 'pdbe':
+            provider = ctx.dataFormat.registry.get('ccp4')
+            break;
+        case 'pdbe-emd-ds':
+        case 'pdbe-xray-ds':
+            provider = ctx.dataFormat.registry.get('dscif')
+            break;
+        case 'rcsb':
+            provider = ctx.dataFormat.registry.get('dsn6')
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
+    }
+
+    const b = state.build().to(data.ref);
+    await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx)
+}));
+
+export const InitVolumeStreaming = StateAction.build({
+    display: { name: 'Volume Streaming' },
+    from: PluginStateObject.Molecule.Structure,
+    params: VolumeStreaming.Params
+})(({ ref, state, params }, ctx: PluginContext) => {
+    // TODO: specify simpler params
+    // TODO: try to determine if the input is x-ray or emd (in params provider)
+    // TODO: for EMD, use PDBe API to determine controur level https://github.com/dsehnal/LiteMol/blob/master/src/Viewer/Extensions/DensityStreaming/Entity.ts#L168
+    // TODO: custom react view for this and the VolumeStreamingBehavior transformer
+
+    const root = state.build().to(ref)
+        .apply(StateTransforms.Volume.VolumeStreamingBehavior, params);
+
+    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: '2FO-FC', level: '2fo-fc' }, { props: { isGhost: true } });
+    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(+ve)' }, { props: { isGhost: true } });
+    root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+
+    return state.updateTree(root);
+});

+ 79 - 0
src/mol-plugin/state/animation/built-in.ts

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateAnimation } from './model';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { StateSelection } from 'mol-state';
+import { PluginCommands } from 'mol-plugin/command';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+export const AnimateModelIndex = PluginStateAnimation.create({
+    name: 'built-in.animate-model-index',
+    display: { name: 'Animate Model Index' },
+    params: () => ({
+        mode: PD.MappedStatic('once', {
+            once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true }),
+            palindrome: PD.Group({ }),
+            loop: PD.Group({ }),
+        }, { options: [['once', 'Once'], ['palindrome', 'Palindrome'], ['loop', 'Loop']] }),
+        maxFPS: PD.Numeric(3, { min: 0.5, max: 30, step: 0.5 })
+    }),
+    initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }),
+    async apply(animState, t, ctx) {
+        // limit fps
+        if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) {
+            return { kind: 'skip' };
+        }
+
+        const state = ctx.plugin.state.dataState;
+        const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+            .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+
+        const update = state.build();
+
+        const params = ctx.params;
+        const palindromeDirections = animState.palindromeDirections || { };
+        let isEnd = false;
+
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                old => {
+                    const len = traj.data.length;
+                    let dir: -1 | 1 = 1;
+                    if (params.mode.name === 'once') {
+                        dir = params.mode.params.direction === 'backward' ? -1 : 1;
+                        // if we are at start or end already, do nothing.
+                        if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
+                            isEnd = true;
+                            return old;
+                        }
+                    } else if (params.mode.name === 'palindrome') {
+                        if (old.modelIndex === 0) dir = 1;
+                        else if (old.modelIndex === len - 1) dir = -1;
+                        else dir = palindromeDirections[m.transform.ref] || 1;
+                    }
+                    palindromeDirections[m.transform.ref] = dir;
+
+                    let modelIndex = (old.modelIndex + dir) % len;
+                    if (modelIndex < 0) modelIndex += len;
+
+                    isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
+
+                    return { modelIndex };
+                });
+        }
+
+        await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, doNotLogTiming: true });
+
+        if (params.mode.name === 'once' && isEnd) return { kind: 'finished' };
+        if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } };
+        return { kind: 'next', state: {} };
+    }
+})

+ 192 - 0
src/mol-plugin/state/animation/manager.ts

@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginComponent } from 'mol-plugin/component';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateAnimation } from './model';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+export { PluginAnimationManager }
+
+// TODO: pause functionality (this needs to reset if the state tree changes)
+// TODO: handle unregistered animations on state restore
+// TODO: better API
+
+class PluginAnimationManager extends PluginComponent<PluginAnimationManager.State> {
+    private map = new Map<string, PluginStateAnimation>();
+    private animations: PluginStateAnimation[] = [];
+    private _current: PluginAnimationManager.Current;
+    private _params?: PD.For<PluginAnimationManager.State['params']> = void 0;
+
+    readonly events = {
+        updated: this.ev()
+    };
+
+    get isEmpty() { return this.animations.length === 0; }
+    get current() { return this._current!; }
+
+    private triggerUpdate() {
+        this.events.updated.next();
+    }
+
+    getParams(): PD.Params {
+        if (!this._params) {
+            this._params = {
+                current: PD.Select(this.animations[0] && this.animations[0].name,
+                    this.animations.map(a => [a.name, a.display.name] as [string, string]),
+                    { label: 'Animation' })
+            };
+        }
+        return this._params as any as PD.Params;
+    }
+
+    updateParams(newParams: Partial<PluginAnimationManager.State['params']>) {
+        this.updateState({ params: { ...this.state.params, ...newParams } });
+        const anim = this.map.get(this.state.params.current)!;
+        const params = anim.params(this.context) as PD.Params;
+        this._current = {
+            anim,
+            params,
+            paramValues: PD.getDefaultValues(params),
+            state: {},
+            startedTime: -1,
+            lastTime: 0
+        }
+        this.triggerUpdate();
+    }
+
+    updateCurrentParams(values: any) {
+        this._current.paramValues = { ...this._current.paramValues, ...values };
+        this.triggerUpdate();
+    }
+
+    register(animation: PluginStateAnimation) {
+        if (this.map.has(animation.name)) {
+            this.context.log.error(`Animation '${animation.name}' is already registered.`);
+            return;
+        }
+        this._params = void 0;
+        this.map.set(animation.name, animation);
+        this.animations.push(animation);
+        if (this.animations.length === 1) {
+            this.updateParams({ current: animation.name });
+        } else {
+            this.triggerUpdate();
+        }
+    }
+
+    play<P>(animation: PluginStateAnimation<P>, params: P) {
+        this.stop();
+        if (!this.map.has(animation.name)) {
+            this.register(animation);
+        }
+        this.updateParams({ current: animation.name });
+        this.updateCurrentParams(params);
+        this.start();
+    }
+
+    start() {
+        this.updateState({ animationState: 'playing' });
+        this.triggerUpdate();
+
+        this._current.lastTime = 0;
+        this._current.startedTime = -1;
+        this._current.state = this._current.anim.initialState(this._current.paramValues, this.context);
+
+        requestAnimationFrame(this.animate);
+    }
+
+    stop() {
+        if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame);
+        this.updateState({ animationState: 'stopped' });
+        this.triggerUpdate();
+    }
+
+    get isAnimating() {
+        return this.state.animationState === 'playing';
+    }
+
+    private _frame: number | undefined = void 0;
+    private animate = async (t: number) => {
+        this._frame = void 0;
+
+        if (this._current.startedTime < 0) this._current.startedTime = t;
+        const newState = await this._current.anim.apply(
+            this._current.state,
+            { lastApplied: this._current.lastTime, current: t - this._current.startedTime },
+            { params: this._current.paramValues, plugin: this.context });
+
+        if (newState.kind === 'finished') {
+            this.stop();
+        } else if (newState.kind === 'next') {
+            this._current.state = newState.state;
+            this._current.lastTime = t - this._current.startedTime;
+            if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
+        } else if (newState.kind === 'skip') {
+            if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate);
+        }
+    }
+
+    getSnapshot(): PluginAnimationManager.Snapshot {
+        if (!this.current) return { state: this.state };
+
+        return {
+            state: this.state,
+            current: {
+                paramValues: this._current.paramValues,
+                state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state
+            }
+        };
+    }
+
+    setSnapshot(snapshot: PluginAnimationManager.Snapshot) {
+        this.updateState({ animationState: snapshot.state.animationState });
+        this.updateParams(snapshot.state.params);
+
+        if (snapshot.current) {
+            this.current.paramValues = snapshot.current.paramValues;
+            this.current.state = this._current.anim.stateSerialization
+                ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state)
+                : snapshot.current.state;
+            this.triggerUpdate();
+            if (this.state.animationState === 'playing') this.resume();
+        }
+    }
+
+    private resume() {
+        this._current.lastTime = 0;
+        this._current.startedTime = -1;
+        requestAnimationFrame(this.animate);
+    }
+
+    constructor(private context: PluginContext) {
+        super({ params: { current: '' }, animationState: 'stopped' });
+    }
+}
+
+namespace PluginAnimationManager {
+    export interface Current {
+        anim: PluginStateAnimation
+        params: PD.Params,
+        paramValues: any,
+        state: any,
+        startedTime: number,
+        lastTime: number
+    }
+
+    export interface State {
+        params: { current: string },
+        animationState: 'stopped' | 'playing'
+    }
+
+    export interface Snapshot {
+        state: State,
+        current?: {
+            paramValues: any,
+            state: any
+        }
+    }
+}

+ 49 - 0
src/mol-plugin/state/animation/model.ts

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginContext } from 'mol-plugin/context';
+
+export { PluginStateAnimation }
+
+// TODO: helpers for building animations (once more animations are added)
+//       for example "composite animation"
+
+interface PluginStateAnimation<P = any, S = any> {
+    name: string,
+    readonly display: { readonly name: string, readonly description?: string },
+    params: (ctx: PluginContext) => PD.For<P>,
+    initialState(params: P, ctx: PluginContext): S,
+
+    /**
+     * Apply the current frame and modify the state.
+     * @param t Current absolute time since the animation started.
+     */
+    apply(state: S, t: PluginStateAnimation.Time, ctx: PluginStateAnimation.Context<P>): Promise<PluginStateAnimation.ApplyResult<S>>,
+
+    /**
+     * The state must be serializable to JSON. If JSON.stringify is not enough,
+     * custom converted to an object that works with JSON.stringify can be provided.
+     */
+    stateSerialization?: { toJSON(state: S): any, fromJSON(data: any): S }
+}
+
+namespace PluginStateAnimation {
+    export interface Time {
+        lastApplied: number,
+        current: number
+    }
+
+    export type ApplyResult<S> = { kind: 'finished' } | { kind: 'skip' } | { kind: 'next', state: S }
+    export interface Context<P> {
+        params: P,
+        plugin: PluginContext
+    }
+
+    export function create<P, S>(params: PluginStateAnimation<P, S>) {
+        return params;
+    }
+}

+ 14 - 18
src/mol-plugin/state/camera.ts

@@ -7,57 +7,53 @@
 import { Camera } from 'mol-canvas3d/camera';
 import { OrderedMap } from 'immutable';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
+import { PluginComponent } from 'mol-plugin/component';
 
 export { CameraSnapshotManager }
 
-class CameraSnapshotManager {
-    private ev = RxEventHelper.create();
-    private _entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
-
+class CameraSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, CameraSnapshotManager.Entry> }> {
     readonly events = {
         changed: this.ev()
     };
 
-    get entries() { return this._entries; }
-
     getEntry(id: string) {
-        return this._entries.get(id);
+        return this.state.entries.get(id);
     }
 
     remove(id: string) {
-        if (!this._entries.has(id)) return;
-        this._entries.delete(id);
+        if (!this.state.entries.has(id)) return;
+        this.updateState({ entries: this.state.entries.delete(id) });
         this.events.changed.next();
     }
 
     add(e: CameraSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.updateState({ entries: this.state.entries.set(e.id, e) });
         this.events.changed.next();
     }
 
     clear() {
-        if (this._entries.size === 0) return;
-        this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
+        if (this.state.entries.size === 0) return;
+        this.updateState({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() });
         this.events.changed.next();
     }
 
     getStateSnapshot(): CameraSnapshotManager.StateSnapshot {
         const entries: CameraSnapshotManager.Entry[] = [];
-        this._entries.forEach(e => entries.push(e!));
+        this.state.entries.forEach(e => entries.push(e!));
         return { entries };
     }
 
     setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) {
-        this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
+        const entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
         for (const e of state.entries) {
-            this._entries.set(e.id, e);
+            entries.set(e.id, e);
         }
+        this.updateState({ entries: entries.asImmutable() });
         this.events.changed.next();
     }
 
-    dispose() {
-        this.ev.dispose();
+    constructor() {
+        super({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() });
     }
 }
 

+ 3 - 3
src/mol-plugin/state/objects.ts

@@ -12,7 +12,7 @@ import { PluginBehavior } from 'mol-plugin/behavior/behavior';
 import { Representation } from 'mol-repr/representation';
 import { StructureRepresentation } from 'mol-repr/structure/representation';
 import { VolumeRepresentation } from 'mol-repr/volume/representation';
-import { StateObject, Transformer } from 'mol-state';
+import { StateObject, StateTransformer } from 'mol-state';
 import { Ccp4File } from 'mol-io/reader/ccp4/schema';
 import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 
@@ -77,6 +77,6 @@ export namespace PluginStateObject {
 }
 
 export namespace PluginStateTransform {
-    export const CreateBuiltIn = Transformer.factory('ms-plugin');
-    export const BuiltIn = Transformer.builderFactory('ms-plugin');
+    export const CreateBuiltIn = StateTransformer.factory('ms-plugin');
+    export const BuiltIn = StateTransformer.builderFactory('ms-plugin');
 }

+ 10 - 15
src/mol-plugin/state/snapshots.ts

@@ -6,44 +6,39 @@
 
 import { OrderedMap } from 'immutable';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { PluginState } from '../state';
+import { PluginComponent } from 'mol-plugin/component';
 
 export { PluginStateSnapshotManager }
 
-class PluginStateSnapshotManager {
-    private ev = RxEventHelper.create();
-    private _entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable();
-
+class PluginStateSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, PluginStateSnapshotManager.Entry> }> {
     readonly events = {
         changed: this.ev()
     };
 
-    get entries() { return this._entries; }
-
     getEntry(id: string) {
-        return this._entries.get(id);
+        return this.state.entries.get(id);
     }
 
     remove(id: string) {
-        if (!this._entries.has(id)) return;
-        this._entries.delete(id);
+        if (!this.state.entries.has(id)) return;
+        this.updateState({ entries: this.state.entries.delete(id) });
         this.events.changed.next();
     }
 
     add(e: PluginStateSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.updateState({ entries: this.state.entries.set(e.id, e) });
         this.events.changed.next();
     }
 
     clear() {
-        if (this._entries.size === 0) return;
-        this._entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable();
+        if (this.state.entries.size === 0) return;
+        this.updateState({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() });
         this.events.changed.next();
     }
 
-    dispose() {
-        this.ev.dispose();
+    constructor() {
+        super({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() });
     }
 }
 

+ 2 - 0
src/mol-plugin/state/transforms.ts

@@ -6,10 +6,12 @@
 
 import * as Data from './transforms/data'
 import * as Model from './transforms/model'
+import * as Volume from './transforms/volume'
 import * as Representation from './transforms/representation'
 
 export const StateTransforms = {
     Data,
     Model,
+    Volume,
     Representation
 }

+ 6 - 6
src/mol-plugin/state/transforms/data.ts

@@ -11,7 +11,7 @@ import { Task } from 'mol-task';
 import CIF from 'mol-io/reader/cif'
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Transformer } from 'mol-state';
+import { StateTransformer } from 'mol-state';
 import { readFromFile } from 'mol-util/data-source';
 import * as CCP4 from 'mol-io/reader/ccp4/parser'
 import * as DSN6 from 'mol-io/reader/dsn6/parser'
@@ -38,12 +38,12 @@ const Download = PluginStateTransform.BuiltIn({
         });
     },
     update({ oldParams, newParams, b }) {
-        if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate;
+        if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate;
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || newParams.url;
-            return Transformer.UpdateResult.Updated;
+            return StateTransformer.UpdateResult.Updated;
         }
-        return Transformer.UpdateResult.Unchanged;
+        return StateTransformer.UpdateResult.Unchanged;
     }
 });
 
@@ -71,9 +71,9 @@ const ReadFile = PluginStateTransform.BuiltIn({
     update({ oldParams, newParams, b }) {
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || oldParams.file.name;
-            return Transformer.UpdateResult.Updated;
+            return StateTransformer.UpdateResult.Updated;
         }
-        return Transformer.UpdateResult.Unchanged;
+        return StateTransformer.UpdateResult.Unchanged;
     },
     isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' })
 });

+ 44 - 112
src/mol-plugin/state/transforms/model.ts

@@ -5,27 +5,31 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { PluginStateTransform } from '../objects';
-import { PluginStateObject as SO } from '../objects';
-import { Task, RuntimeContext } from 'mol-task';
-import { Model, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { parsePDB } from 'mol-io/reader/pdb/parser';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
+import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
+import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry } from 'mol-model/structure';
+import { Assembly } from 'mol-model/structure/model/properties/symmetry';
+import { PluginContext } from 'mol-plugin/context';
+import { MolScriptBuilder } from 'mol-script/language/builder';
 import Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
-import { MolScriptBuilder } from 'mol-script/language/builder';
 import { StateObject } from 'mol-state';
-import { PluginContext } from 'mol-plugin/context';
+import { RuntimeContext, Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { stringToWords } from 'mol-util/string';
-import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
-import { Vec3 } from 'mol-math/linear-algebra';
-import CIF from 'mol-io/reader/cif';
-import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
-import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
-import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
-import { parsePDB } from 'mol-io/reader/pdb/parser';
-import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
 
-export { TrajectoryFromMmCif }
+export { TrajectoryFromMmCif };
+export { TrajectoryFromPDB };
+export { ModelFromTrajectory };
+export { StructureFromModel };
+export { StructureAssemblyFromModel };
+export { StructureSymmetryFromModel };
+export { StructureSelection };
+export { StructureComplexElement };
+export { CustomModelProperties };
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
@@ -59,11 +63,10 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
 });
 
 
-export { TrajectoryFromPDB }
 type TrajectoryFromPDB = typeof TrajectoryFromPDB
 const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-pdb',
-    display: { name: 'Parse PDB string and create trajectory' },
+    display: { name: 'Parse PDB', description: 'Parse PDB string and create trajectory.' },
     from: [SO.Data.String],
     to: SO.Molecule.Trajectory
 })({
@@ -79,12 +82,11 @@ const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
 });
 
 
-export { ModelFromTrajectory }
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     name: 'model-from-trajectory',
-    display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' },
+    display: { name: 'Molecular Model', description: 'Create a molecular model from specified index in a trajectory.' },
     from: SO.Molecule.Trajectory,
     to: SO.Molecule.Model,
     params: a => {
@@ -98,12 +100,13 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         const model = a.data[params.modelIndex];
-        const props = { label: `Model ${model.modelNum}` };
+        const props = a.data.length === 1
+            ? { label: `${model.label}` }
+            : { label: `${model.label}:${model.modelNum}`, description: `Model ${model.modelNum} of ${a.data.length}` };
         return new SO.Molecule.Model(model, props);
     }
 });
 
-export { StructureFromModel }
 type StructureFromModel = typeof StructureFromModel
 const StructureFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-from-model',
@@ -122,7 +125,6 @@ function structureDesc(s: Structure) {
     return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
 }
 
-export { StructureAssemblyFromModel }
 type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
 const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-assembly-from-model',
@@ -143,17 +145,30 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
         return Task.create('Build Assembly', async ctx => {
             const model = a.data;
             let id = params.id;
-            let asm = ModelSymmetry.findAssembly(model, id || '');
-            if (!!id && id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`);
+            let asm: Assembly | undefined = void 0;
+
+            // if no id is specified, use the 1st assembly.
+            if (!id && model.symmetry.assemblies.length !== 0) {
+                id = model.symmetry.assemblies[0].id;
+            }
+
+            if (model.symmetry.assemblies.length === 0) {
+                if (id !== 'deposited') {
+                    plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`);
+                }
+            } else {
+                asm = ModelSymmetry.findAssembly(model, id || '');
+                if (!asm) {
+                    plugin.log.warn(`Model '${a.label}' has no assembly called '${id}', returning deposited structure.`);
+                }
+            }
 
             const base = Structure.ofModel(model);
-            if ((id && !asm) || model.symmetry.assemblies.length === 0) {
-                if (!!id && id !== 'deposited') plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`);
+            if (!asm) {
                 const label = { label: a.data.label, description: structureDesc(base) };
                 return new SO.Molecule.Structure(base, label);
             }
 
-            asm = model.symmetry.assemblies[0];
             id = asm.id;
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
             const props = { label: `Assembly ${id}`, description: structureDesc(s) };
@@ -162,7 +177,6 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureSymmetryFromModel }
 type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
 const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-symmetry-from-model',
@@ -188,7 +202,6 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureSelection }
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
@@ -210,7 +223,6 @@ const StructureSelection = PluginStateTransform.BuiltIn({
     }
 });
 
-export { StructureComplexElement }
 namespace StructureComplexElement {
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
 }
@@ -243,7 +255,6 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
     }
 });
 
-export { CustomModelProperties }
 type CustomModelProperties = typeof CustomModelProperties
 const CustomModelProperties = PluginStateTransform.BuiltIn({
     name: 'custom-model-properties',
@@ -267,83 +278,4 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon
         const p = ctx.customModelProperties.get(name);
         await p.attach(model).runInContext(taskCtx);
     }
-}
-
-//
-
-export { VolumeFromCcp4 }
-type VolumeFromCcp4 = typeof VolumeFromCcp4
-const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
-    name: 'volume-from-ccp4',
-    display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' },
-    from: SO.Format.Ccp4,
-    to: SO.Volume.Data,
-    params(a) {
-        return {
-            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
-        };
-    }
-})({
-    apply({ a, params }) {
-        return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
-            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx)
-            const props = { label: 'Volume' };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
-
-export { VolumeFromDsn6 }
-type VolumeFromDsn6 = typeof VolumeFromDsn6
-const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
-    name: 'volume-from-dsn6',
-    display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' },
-    from: SO.Format.Dsn6,
-    to: SO.Volume.Data,
-    params(a) {
-        return {
-            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
-        };
-    }
-})({
-    apply({ a, params }) {
-        return Task.create('Create volume from DSN6/BRIX', async ctx => {
-            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx)
-            const props = { label: 'Volume' };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
-
-export { VolumeFromDensityServerCif }
-type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
-const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
-    name: 'volume-from-density-server-cif',
-    display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' },
-    from: SO.Format.Cif,
-    to: SO.Volume.Data,
-    params(a) {
-        if (!a) {
-            return {
-                blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
-            };
-        }
-        const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
-        return {
-            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
-        };
-    }
-})({
-    isApplicable: a => a.data.blocks.length > 0,
-    apply({ a, params }) {
-        return Task.create('Parse density-server CIF', async ctx => {
-            const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data
-            const block = a.data.blocks.find(b => b.header === header);
-            if (!block) throw new Error(`Data block '${[header]}' not found.`);
-            const densityServerCif = CIF.schema.densityServer(block)
-            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx)
-            const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
-            return new SO.Volume.Data(volume, props);
-        });
-    }
-});
+}

+ 26 - 21
src/mol-plugin/state/transforms/representation.ts

@@ -5,7 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Transformer } from 'mol-state';
+import { StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
 import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } from '../objects';
@@ -16,12 +16,14 @@ import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry
 import { Structure } from 'mol-model/structure';
 import { StructureParams } from 'mol-repr/structure/representation';
 import { ExplodeRepresentation3D } from 'mol-plugin/behavior/dynamic/representation';
-import { VolumeData } from 'mol-model/volume';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
 import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry';
 import { VolumeParams } from 'mol-repr/volume/representation';
+import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color';
+import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size';
 
 export namespace StructureRepresentation3DHelpers {
-    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
+    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
 
         const themeDataCtx = { structure };
@@ -35,7 +37,7 @@ export namespace StructureRepresentation3DHelpers {
         })
     }
 
-    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
         const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
         const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues
@@ -112,11 +114,11 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
-            if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate;
+            if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
             const props = { ...b.data.props, ...newParams.type.params }
             b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams))
             await b.data.createOrUpdate(props, a.data).runInContext(ctx);
-            return Transformer.UpdateResult.Updated;
+            return StateTransformer.UpdateResult.Updated;
         });
     }
 });
@@ -140,7 +142,7 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
         return Task.create('Update Explosion', async () => {
             const updated = await b.data.update(newParams);
             b.label = `Explosion T = ${newParams.t.toFixed(2)}`;
-            return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged;
+            return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged;
         });
     }
 });
@@ -148,7 +150,7 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({
 //
 
 export namespace VolumeRepresentation3DHelpers {
-    export function getDefaultParams(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volume: VolumeData, volumeParams?: Partial<PD.Values<VolumeParams>>): Transformer.Params<VolumeRepresentation3D> {
+    export function getDefaultParams(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volume: VolumeData, volumeParams?: Partial<PD.Values<VolumeParams>>): StateTransformer.Params<VolumeRepresentation3D> {
         const type = ctx.volumeRepresentation.registry.get(name);
 
         const themeDataCtx = { volume };
@@ -162,16 +164,20 @@ export namespace VolumeRepresentation3DHelpers {
         })
     }
 
-    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volumeParams?: Partial<PD.Values<VolumeParams>>): Transformer.Params<VolumeRepresentation3D> {
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volumeParams?: Partial<PD.Values<PD.Params>>, colorName?: BuiltInColorThemeName, colorParams?: Partial<ColorTheme.Props>, sizeName?: BuiltInSizeThemeName, sizeParams?: Partial<SizeTheme.Props>): StateTransformer.Params<VolumeRepresentation3D> {
         const type = ctx.volumeRepresentation.registry.get(name);
-        const colorParams = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
-        const sizeParams = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues
+        const colorType = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(colorName || type.defaultColorTheme);
+        const sizeType = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(sizeName || type.defaultSizeTheme);
         return ({
             type: { name, params: volumeParams ? { ...type.defaultValues, ...volumeParams } : type.defaultValues },
-            colorTheme: { name: type.defaultColorTheme, params: colorParams },
-            sizeTheme: { name: type.defaultSizeTheme, params: sizeParams }
+            colorTheme: { name: type.defaultColorTheme, params: colorParams ? { ...colorType.defaultValues, ...colorParams } : colorType.defaultValues },
+            sizeTheme: { name: type.defaultSizeTheme, params: sizeParams ? { ...sizeType.defaultValues, ...sizeParams } : sizeType.defaultValues }
         })
     }
+
+    export function getDescription(props: any) {
+        return props.isoValue && VolumeIsoValue.toString(props.isoValue)
+    }
 }
 export { VolumeRepresentation3D }
 type VolumeRepresentation3D = typeof VolumeRepresentation3D
@@ -189,16 +195,16 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
                 type: PD.Mapped<any>(
                     registry.default.name,
                     registry.types,
-                    name => PD.Group<any>(registry.get(name).getParams(themeCtx, VolumeData.Empty ))),
+                    name => PD.Group<any>(registry.get(name).getParams(themeCtx, VolumeData.One ))),
                 colorTheme: PD.Mapped<any>(
                     type.defaultColorTheme,
                     themeCtx.colorThemeRegistry.types,
-                    name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ volume: VolumeData.Empty }))
+                    name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ volume: VolumeData.One }))
                 ),
                 sizeTheme: PD.Mapped<any>(
                     type.defaultSizeTheme,
                     themeCtx.sizeThemeRegistry.types,
-                    name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ volume: VolumeData.Empty }))
+                    name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ volume: VolumeData.One }))
                 )
             }
         }
@@ -233,19 +239,18 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
             const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
             repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
             // TODO set initial state, repr.setState({})
-            // TODO include isoValue in the label where available
-            console.log(params.type.params);
             await repr.createOrUpdate(props, a.data).runInContext(ctx);
-            return new SO.Volume.Representation3D(repr, { label: provider.label });
+            return new SO.Volume.Representation3D(repr, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Volume Representation', async ctx => {
-            if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate;
+            if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate;
             const props = { ...b.data.props, ...newParams.type.params }
             b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
             await b.data.createOrUpdate(props, a.data).runInContext(ctx);
-            return Transformer.UpdateResult.Updated;
+            b.description = VolumeRepresentation3DHelpers.getDescription(props)
+            return StateTransformer.UpdateResult.Updated;
         });
     }
 });

+ 195 - 0
src/mol-plugin/state/transforms/volume.ts

@@ -0,0 +1,195 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import CIF from 'mol-io/reader/cif';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
+import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6';
+import { Task } from 'mol-task';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginStateObject as SO, PluginStateTransform } from '../objects';
+import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume';
+import { PluginContext } from 'mol-plugin/context';
+import { StateTransformer } from 'mol-state';
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume';
+import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry';
+import { createTheme } from 'mol-theme/theme';
+import { VolumeRepresentation3DHelpers } from './representation';
+import { Color } from 'mol-util/color';
+
+export { VolumeFromCcp4 };
+export { VolumeFromDsn6 };
+export { VolumeFromDensityServerCif };
+type VolumeFromCcp4 = typeof VolumeFromCcp4
+const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-ccp4',
+    display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' },
+    from: SO.Format.Ccp4,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from CCP4/MRC/MAP', async ctx => {
+            const volume = await volumeFromCcp4(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDsn6 = typeof VolumeFromDsn6
+const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
+    name: 'volume-from-dsn6',
+    display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' },
+    from: SO.Format.Dsn6,
+    to: SO.Volume.Data,
+    params(a) {
+        return {
+            voxelSize: PD.Vec3(Vec3.create(1, 1, 1))
+        };
+    }
+})({
+    apply({ a, params }) {
+        return Task.create('Create volume from DSN6/BRIX', async ctx => {
+            const volume = await volumeFromDsn6(a.data, params).runInContext(ctx)
+            const props = { label: 'Volume' };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif
+const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
+    name: 'volume-from-density-server-cif',
+    display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' },
+    from: SO.Format.Cif,
+    to: SO.Volume.Data,
+    params(a) {
+        if (!a) {
+            return {
+                blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' }))
+            };
+        }
+        const blocks = a.data.blocks.slice(1); // zero block contains query meta-data
+        return {
+            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
+        };
+    }
+})({
+    isApplicable: a => a.data.blocks.length > 0,
+    apply({ a, params }) {
+        return Task.create('Parse density-server CIF', async ctx => {
+            const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data
+            const block = a.data.blocks.find(b => b.header === header);
+            if (!block) throw new Error(`Data block '${[header]}' not found.`);
+            const densityServerCif = CIF.schema.densityServer(block)
+            const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx)
+            const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` };
+            return new SO.Volume.Data(volume, props);
+        });
+    }
+});
+
+export { VolumeStreamingBehavior }
+type VolumeStreamingBehavior = typeof VolumeStreamingBehavior
+const VolumeStreamingBehavior = PluginStateTransform.BuiltIn({
+    name: 'volume-streaming-behavior',
+    display: { name: 'Volume Streaming Behavior', description: 'Create Volume Streaming behavior.' },
+    from: SO.Molecule.Structure,
+    to: VolumeStreaming.Obj,
+    params: VolumeStreaming.Params
+})({
+    canAutoUpdate: ({ oldParams, newParams }) => oldParams.serverUrl === newParams.serverUrl && oldParams.id === newParams.id,
+    apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume Streaming', async ctx => {
+        const behavior = new VolumeStreaming.Behavior(plugin, params, a.data);
+        // get the initial data now so that the child projections dont get empty volumes.
+        await behavior.update(behavior.params);
+        return new VolumeStreaming.Obj(behavior, { label: 'Volume Streaming' });
+    }),
+    update({ b, newParams }) {
+        return Task.create('Update Volume Streaming', async _ => {
+            await b.data.update(newParams);
+            return StateTransformer.UpdateResult.Updated;
+        });
+    }
+});
+
+// export { VolumeStreamingData }
+// type VolumeStreamingData = typeof VolumeStreamingData
+// const VolumeStreamingData = PluginStateTransform.BuiltIn({
+//     name: 'volume-streaming-data',
+//     display: { name: 'Volume Streaming Data' },
+//     from: VolumeStreaming.Obj,
+//     to: SO.Volume.Data,
+//     params: {
+//         channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }),
+//         level: PD.Text<VolumeStreaming.LevelType>('em')
+//     }
+// })({
+//     apply({ a, params }, plugin: PluginContext) {
+//         const data = a.data.currentData[params.channel] || VolumeData.Empty;
+//         console.log({ data });
+//         return new SO.Volume.Data(a.data.currentData[params.channel] || VolumeData.Empty, { label: params.level });
+//     }
+// });
+
+export { VolumeStreamingVisual }
+type VolumeStreamingVisual = typeof VolumeStreamingVisual
+const VolumeStreamingVisual = PluginStateTransform.BuiltIn({
+    name: 'volume-streaming-visual',
+    display: { name: 'Volume Streaming Visual' },
+    from: VolumeStreaming.Obj,
+    to: SO.Volume.Representation3D,
+    params: {
+        channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }),
+        level: PD.Text<VolumeStreaming.LevelType>('em')
+    }
+})({
+    apply: ({ a, params: srcParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        const { data, params } = createVolumeProps(a.data, srcParams.channel, srcParams.level);
+
+        const provider = BuiltInVolumeRepresentations.isosurface;
+        const props = params.type.params || {}
+        const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
+        repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params))
+        await repr.createOrUpdate(props, data).runInContext(ctx);
+        return new SO.Volume.Representation3D(repr, { label: srcParams.level, description: VolumeRepresentation3DHelpers.getDescription(props) });
+    }),
+    update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => {
+        // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work
+        const { data, params } = createVolumeProps(a.data, newParams.channel, newParams.level);
+        const props = { ...b.data.props, ...params.type.params };
+        b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params))
+        await b.data.createOrUpdate(props, data).runInContext(ctx);
+        return StateTransformer.UpdateResult.Updated;
+    })
+});
+
+function createVolumeProps(streaming: VolumeStreaming.Behavior, channel: keyof VolumeStreaming.ChannelData, level: VolumeStreaming.LevelType) {
+    const data = streaming.currentData[channel] || VolumeData.One;
+    // TODO: createTheme fails when VolumeData.Empty is used for some reason.
+
+    let isoValue: VolumeIsoValue, color: Color;
+
+    if (level === 'em' && streaming.params.levels.name === 'em') {
+        isoValue = streaming.params.levels.params.isoValue;
+        color = streaming.params.levels.params.color;
+    } else if (level !== 'em' && streaming.params.levels.name === 'x-ray') {
+        isoValue = streaming.params.levels.params[level].isoValue;
+        color = streaming.params.levels.params[level].color;
+    } else {
+        throw new Error(`Unsupported iso level ${level}.`);
+    }
+
+    const params = VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.ctx, 'isosurface', { isoValue, alpha: 0.3 }, 'uniform', { value: color });
+    return { data, params };
+}

+ 2 - 2
src/mol-plugin/ui/base.tsx

@@ -10,7 +10,7 @@ import { PluginContext } from '../context';
 
 export const PluginReactContext = React.createContext(void 0 as any as PluginContext);
 
-export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> {
+export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> {
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
 
@@ -35,7 +35,7 @@ export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Com
     }
 }
 
-export abstract class PurePluginComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> {
+export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> {
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
 

+ 5 - 5
src/mol-plugin/ui/camera.tsx

@@ -6,11 +6,11 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 
-export class CameraSnapshots extends PluginComponent<{ }, { }> {
+export class CameraSnapshots extends PluginUIComponent<{ }, { }> {
     render() {
         return <div>
             <div className='msp-section-header'>Camera Snapshots</div>
@@ -20,7 +20,7 @@ export class CameraSnapshots extends PluginComponent<{ }, { }> {
     }
 }
 
-class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> {
+class CameraSnapshotControls extends PluginUIComponent<{ }, { name: string, description: string }> {
     static Params = {
         name: PD.Text(),
         description: PD.Text()
@@ -48,7 +48,7 @@ class CameraSnapshotControls extends PluginComponent<{ }, { name: string, descri
     }
 }
 
-class CameraSnapshotList extends PluginComponent<{ }, { }> {
+class CameraSnapshotList extends PluginUIComponent<{ }, { }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate());
     }
@@ -65,7 +65,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> {
 
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-            {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}>
+            {this.plugin.state.cameraSnapshots.state.entries.valueSeq().map(e =><li key={e!.id}>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
                 <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />

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

@@ -6,11 +6,11 @@
 
 import * as React from 'react';
 import { PluginCommands } from 'mol-plugin/command';
-import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
-import { PluginComponent } from './base';
+import { UpdateTrajectory } from 'mol-plugin/state/actions/structure';
+import { PluginUIComponent } from './base';
 import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager';
 
-export class Controls extends PluginComponent<{ }, { }> {
+export class Controls extends PluginUIComponent<{ }, { }> {
     render() {
         return <>
 
@@ -18,7 +18,7 @@ export class Controls extends PluginComponent<{ }, { }> {
     }
 }
 
-export class TrajectoryControls extends PluginComponent {
+export class TrajectoryControls extends PluginUIComponent {
     render() {
         return <div>
             <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
@@ -37,7 +37,7 @@ export class TrajectoryControls extends PluginComponent {
     }
 }
 
-export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
+export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> {
     state = { entries: [] }
 
     componentDidMount() {

+ 55 - 0
src/mol-plugin/ui/controls/common.tsx

@@ -27,6 +27,61 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan
     }
 }
 
+export class NumericInput extends React.PureComponent<{
+    value: number,
+    onChange: (v: number) => void,
+    onEnter?: () => void,
+    onBlur?: () => void,
+    blurOnEnter?: boolean,
+    isDisabled?: boolean,
+    placeholder?: string
+}, { value: string }> {
+    state = { value: '0' };
+    input = React.createRef<HTMLInputElement>();
+
+    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+        const value = +e.target.value;
+        this.setState({ value: e.target.value }, () => {
+            if (!Number.isNaN(value) && value !== this.props.value) {
+                this.props.onChange(value);
+            }
+        });
+    }
+
+    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
+        if ((e.keyCode === 13 || e.charCode === 13)) {
+            if (this.props.blurOnEnter && this.input.current) {
+                this.input.current.blur();
+            }
+            if (this.props.onEnter) this.props.onEnter();
+        }
+    }
+
+    onBlur = () => {
+        this.setState({ value: '' + this.props.value });
+        if (this.props.onBlur) this.props.onBlur();
+    }
+
+    static getDerivedStateFromProps(props: { value: number }, state: { value: string }) {
+        const value = +state.value;
+        if (Number.isNaN(value) || value === props.value) return null;
+        return { value: '' + props.value };
+    }
+
+    render() {
+        return <input type='text'
+            ref={this.input}
+            onBlur={this.onBlur}
+            value={this.state.value}
+            placeholder={this.props.placeholder}
+            onChange={this.onChange}
+            onKeyPress={this.props.onEnter || this.props.blurOnEnter ? this.onKeyPress : void 0}
+            disabled={!!this.props.isDisabled}
+        />
+    }
+}
+
+
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,
 //     value: boolean,

+ 17 - 41
src/mol-plugin/ui/controls/parameters.tsx

@@ -15,6 +15,7 @@ import { camelCaseToWords } from 'mol-util/string';
 import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
+import { NumericInput } from './common';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
@@ -28,15 +29,17 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
     render() {
         const params = this.props.params;
         const values = this.props.values;
-        return <div style={{ width: '100%' }}>
-            {Object.keys(params).map(key => {
+        const keys = Object.keys(params);
+        if (keys.length === 0) return null;
+        return <>
+            {keys.map(key => {
                 const param = params[key];
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 if (!Control) return null;
                 return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} />
             })}
-        </div>;
+        </>;
     }
 }
 
@@ -152,53 +155,22 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
     }
 }
 
-export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>, { value: string }> {
+export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> {
     state = { value: '0' };
 
-    protected update(value: any) {
+    update = (value: number) => {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
     }
 
-    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        const value = +e.target.value;
-        this.setState({ value: e.target.value }, () => {
-            if (!Number.isNaN(value) && value !== this.props.value) {
-                this.update(value);
-            }
-        });
-    }
-
-    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if (!this.props.onEnter) return;
-        if ((e.keyCode === 13 || e.charCode === 13)) {
-            this.props.onEnter();
-        }
-    }
-
-    onBlur = () => {
-        this.setState({ value: '' + this.props.value });
-    }
-
-    static getDerivedStateFromProps(props: { value: number }, state: { value: string }) {
-        const value = +state.value;
-        if (Number.isNaN(value) || value === props.value) return null;
-        return { value: '' + props.value };
-    }
-
     render() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <div className='msp-control-row'>
             <span title={this.props.param.description}>{label}</span>
             <div>
-                <input type='text'
-                    onBlur={this.onBlur}
-                    value={this.state.value}
-                    placeholder={placeholder}
-                    onChange={this.onChange}
-                    onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
-                    disabled={this.props.isDisabled}
-                />
+                <NumericInput
+                    value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder}
+                    isDisabled={this.props.isDisabled} onChange={this.update} />
             </div>
         </div>;
     }
@@ -208,7 +180,7 @@ export class NumberRangeControl extends SimpleParam<PD.Numeric> {
     onChange = (v: number) => { this.update(v); }
     renderControl() {
         return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
-            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />
+            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />
     }
 }
 
@@ -267,7 +239,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
     onChange = (v: [number, number]) => { this.update(v); }
     renderControl() {
         return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
-            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />;
+            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
     }
 }
 
@@ -464,6 +436,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
 
     render() {
         const params = this.props.param.params;
+
+        // Do not show if there are no params.
+        if (Object.keys(params).length === 0) return null;
+
         const label = this.props.param.label || camelCaseToWords(this.props.name);
 
         const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;

+ 66 - 27
src/mol-plugin/ui/controls/slider.tsx

@@ -5,6 +5,8 @@
  */
 
 import * as React from 'react'
+import { NumericInput } from './common';
+import { noop } from 'mol-util';
 
 export class Slider extends React.Component<{
     min: number,
@@ -12,7 +14,8 @@ export class Slider extends React.Component<{
     value: number,
     step?: number,
     onChange: (v: number) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: number }> {
 
     state = { isChanging: false, current: 0 }
@@ -35,18 +38,35 @@ export class Slider extends React.Component<{
         this.setState({ current });
     }
 
+    updateManually = (v: number) => {
+        this.setState({ isChanging: true });
+
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.props.min) n = this.props.min;
+        if (n > this.props.max) n = this.props.max;
+
+        this.setState({ current: n, isChanging: true });
+    }
+
+    onManualBlur = () => {
+        this.setState({ isChanging: false });
+        this.props.onChange(this.state.current);
+    }
+
     render() {
         let step = this.props.step;
         if (step === void 0) step = 1;
         return <div className='msp-slider'>
             <div>
-                <div>
-                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
-                        onBeforeChange={this.begin}
-                        onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
-                </div></div>
+                <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                    onBeforeChange={this.begin}
+                    onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
+            </div>
             <div>
-                {`${Math.round(100 * this.state.current) / 100}`}
+                <NumericInput
+                    value={this.state.current} blurOnEnter={true} onBlur={this.onManualBlur}
+                    isDisabled={this.props.disabled} onChange={this.updateManually} />
             </div>
         </div>;
     }
@@ -58,7 +78,8 @@ export class Slider2 extends React.Component<{
     value: [number, number],
     step?: number,
     onChange: (v: [number, number]) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: [number, number] }> {
 
     state = { isChanging: false, current: [0, 1] as [number, number] }
@@ -81,20 +102,41 @@ export class Slider2 extends React.Component<{
         this.setState({ current });
     }
 
+    updateMax = (v: number) => {
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.state.current[0]) n = this.state.current[0]
+        else if (n < this.props.min) n = this.props.min;
+        if (n > this.props.max) n = this.props.max;
+        this.props.onChange([this.state.current[0], n]);
+    }
+
+    updateMin = (v: number) => {
+        let n = v;
+        if (this.props.step === 1) n = Math.round(n);
+        if (n < this.props.min) n = this.props.min;
+        if (n > this.state.current[1]) n = this.state.current[1];
+        else if (n > this.props.max) n = this.props.max;
+        this.props.onChange([n, this.state.current[1]]);
+    }
+
     render() {
         let step = this.props.step;
         if (step === void 0) step = 1;
         return <div className='msp-slider2'>
             <div>
-                {`${Math.round(100 * this.state.current[0]) / 100}`}
+                <NumericInput
+                    value={this.state.current[0]} onEnter={this.props.onEnter} blurOnEnter={true}
+                    isDisabled={this.props.disabled} onChange={this.updateMin} />
             </div>
             <div>
-                <div>
-                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
-                        onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} />
-                </div></div>
+                <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                    onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} />
+            </div>
             <div>
-                {`${Math.round(100 * this.state.current[1]) / 100}`}
+                <NumericInput
+                    value={this.state.current[1]} onEnter={this.props.onEnter} blurOnEnter={true}
+                    isDisabled={this.props.disabled} onChange={this.updateMax} />
             </div>
         </div>;
     }
@@ -102,10 +144,10 @@ export class Slider2 extends React.Component<{
 
 /**
  * The following code was adapted from react-components/slider library.
- * 
+ *
  * The MIT License (MIT)
  * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
- * 
+ *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
@@ -116,12 +158,12 @@ export class Slider2 extends React.Component<{
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
 
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
- * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
- * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
- * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
- * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
 
@@ -151,9 +193,6 @@ function classNames(_classes: { [name: string]: boolean | number }) {
     return classes.join(' ');
 }
 
-function noop() {
-}
-
 function isNotTouchEvent(e: TouchEvent) {
     return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0);
 }
@@ -540,7 +579,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         }
         return false;
 
-        // return this.state.bounds.some((x, i) => e.target 
+        // return this.state.bounds.some((x, i) => e.target
 
         // (
         //     //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i])
@@ -702,7 +741,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
             dragging: handle === i,
             index: i,
             key: i,
-            ref: (h: any) => this.handleElements.push(h)  //`handle-${i}`,
+            ref: (h: any) => this.handleElements.push(h)  // `handle-${i}`,
         }));
         if (!range) { handles.shift(); }
 

+ 49 - 41
src/mol-plugin/ui/plugin.tsx

@@ -4,22 +4,22 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
+import { List } from 'immutable';
+import { PluginState } from 'mol-plugin/state';
+import { formatTime } from 'mol-util';
+import { LogEntry } from 'mol-util/log-entry';
 import * as React from 'react';
 import { PluginContext } from '../context';
-import { StateTree } from './state-tree';
-import { Viewport, ViewportControls } from './viewport';
-import { Controls, TrajectoryControls, LociLabelControl } from './controls';
-import { PluginComponent, PluginReactContext } from './base';
+import { PluginReactContext, PluginUIComponent } from './base';
 import { CameraSnapshots } from './camera';
+import { Controls, LociLabelControl, TrajectoryControls } from './controls';
 import { StateSnapshots } from './state';
-import { List } from 'immutable';
-import { LogEntry } from 'mol-util/log-entry';
-import { formatTime } from 'mol-util';
+import { StateObjectActions } from './state/actions';
+import { AnimationControls } from './state/animation';
+import { StateTree } from './state/tree';
 import { BackgroundTaskProgress } from './task';
-import { ApplyActionContol } from './state/apply-action';
-import { PluginState } from 'mol-plugin/state';
-import { UpdateTransformContol } from './state/update-transform';
-import { StateObjectCell } from 'mol-state';
+import { Viewport, ViewportControls } from './viewport';
+import { StateTransform } from 'mol-state';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
 
@@ -38,9 +38,9 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
     }
 }
 
-class Layout extends PluginComponent {
+class Layout extends PluginUIComponent {
     componentDidMount() {
-        this.subscribe(this.plugin.layout.updated, () => this.forceUpdate());
+        this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
     }
 
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -52,7 +52,7 @@ class Layout extends PluginComponent {
     }
 
     render() {
-        const layout = this.plugin.layout.latestState;
+        const layout = this.plugin.layout.state;
         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'}>
@@ -61,6 +61,7 @@ class Layout extends PluginComponent {
                     {layout.showControls && this.region('right', <div className='msp-scrollable-container msp-right-controls'>
                         <CurrentObject />
                         <Controls />
+                        <AnimationControls />
                         <CameraSnapshots />
                         <StateSnapshots />
                     </div>)}
@@ -71,7 +72,7 @@ class Layout extends PluginComponent {
     }
 }
 
-export class ViewportWrapper extends PluginComponent {
+export class ViewportWrapper extends PluginUIComponent {
     render() {
         return <>
             <Viewport />
@@ -89,7 +90,7 @@ export class ViewportWrapper extends PluginComponent {
     }
 }
 
-export class State extends PluginComponent {
+export class State extends PluginUIComponent {
     componentDidMount() {
         this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
     }
@@ -103,26 +104,26 @@ export class State extends PluginComponent {
         const kind = this.plugin.state.behavior.kind.value;
         return <div className='msp-scrollable-container'>
             <div className='msp-btn-row-group msp-data-beh'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}>Data</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>Behavior</button>
             </div>
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
         </div>
     }
 }
 
-export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
+export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
 
     componentDidMount() {
-        this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries.takeLast(100).toList() }));
+        this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries }));
     }
 
     componentDidUpdate() {
         this.scrollToBottom();
     }
 
-    state = { entries: this.plugin.log.entries.takeLast(100).toList() };
+    state = { entries: this.plugin.log.entries };
 
     private scrollToBottom() {
         const log = this.wrapper.current;
@@ -130,19 +131,26 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     }
 
     render() {
+        // TODO: ability to show full log
+        // showing more entries dramatically slows animations.
+        const maxEntries = 10;
+        const xs = this.state.entries, l = xs.size;
+        const entries: JSX.Element[] = [];
+        for (let i = Math.max(0, l - maxEntries), o = 0; i < l; i++) {
+            const e = xs.get(i);
+            entries.push(<li key={o++}>
+                <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
+                <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
+                <div className='msp-log-entry'>{e!.message}</div>
+            </li>);
+        }
         return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
-            <ul className='msp-list-unstyled'>
-                {this.state.entries.map((e, i) => <li key={i}>
-                    <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
-                    <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
-                    <div className='msp-log-entry'>{e!.message}</div>
-                </li>)}
-            </ul>
+            <ul className='msp-list-unstyled'>{entries}</ul>
         </div>;
     }
 }
 
-export class CurrentObject extends PluginComponent {
+export class CurrentObject extends PluginUIComponent {
     get current() {
         return this.plugin.state.behavior.currentObject.value;
     }
@@ -163,21 +171,21 @@ export class CurrentObject extends PluginComponent {
         const current = this.current;
         const ref = current.ref;
         const cell = current.state.cells.get(ref)!;
-        const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0;
-
-        const type = cell && cell.obj ? cell.obj.type : void 0;
         const transform = cell.transform;
         const def = transform.transformer.definition;
+        const display = cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name;
 
-        const actions = type ? current.state.actions.fromType(type) : [];
-        return <>
-            <div className='msp-section-header'>
-                {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name}
-            </div>
-            { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> }
-            {cell.status === 'ok' &&
-                actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)
-            }
+        let showActions = true;
+        if (ref === StateTransform.RootRef) {
+            const children = current.state.tree.children.get(ref);
+            showActions = children.size !== 0;
+        }
+
+        if (!showActions) return null;
+
+        return cell.status === 'ok' && <>
+            <div className='msp-section-header'>{`Actions (${display})`}</div>
+            <StateObjectActions state={current.state} nodeRef={ref} />
         </>;
     }
 }

+ 6 - 6
src/mol-plugin/ui/state.tsx

@@ -6,14 +6,14 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { shallowEqual } from 'mol-util';
 import { List } from 'immutable';
 import { ParameterControls } from './controls/parameters';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 
-export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> {
+export class StateSnapshots extends PluginUIComponent<{ }, { serverUrl: string }> {
     state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' }
 
     updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
@@ -31,7 +31,7 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }>
 // TODO: this is not nice: device some custom event system.
 const UploadedEvent = new Subject();
 
-class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> {
+class StateSnapshotControls extends PluginUIComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> {
     state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
 
     static Params = {
@@ -93,7 +93,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC
     }
 }
 
-class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
+class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
     }
@@ -110,7 +110,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
 
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
-            {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}>
+            {this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.id}>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button>
                 <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
@@ -121,7 +121,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
 }
 
 type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string }
-class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
+class RemoteStateSnapshotList extends PluginUIComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> {
     state = { entries: List<RemoteEntry>(), isFetching: false };
 
     componentDidMount() {

+ 35 - 0
src/mol-plugin/ui/state/actions.tsx

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 - 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from '../base';
+import { ApplyActionContol } from './apply-action';
+import { State } from 'mol-state';
+
+export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRef: string }> {
+    get current() {
+        return this.plugin.state.behavior.currentObject.value;
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.state.behavior.currentObject, o => {
+            this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+            const current = this.current;
+            if (current.ref !== ref || current.state !== state) return;
+            this.forceUpdate();
+        });
+    }
+
+    render() {
+        const { state, nodeRef: ref } = this.props;
+        const cell = state.cells.get(ref)!;
+        const actions = state.actions.fromCell(cell, this.plugin);
+        return actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={state} action={act} nodeRef={ref} />);
+    }
+}

+ 49 - 0
src/mol-plugin/ui/state/animation.tsx

@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from '../base';
+import { ParameterControls, ParamOnChange } from '../controls/parameters';
+
+export class AnimationControls extends PluginUIComponent<{ }> {
+    componentDidMount() {
+        this.subscribe(this.plugin.state.animation.events.updated, () => this.forceUpdate());
+    }
+
+    updateParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateParams({ [p.name]: p.value });
+    }
+
+    updateCurrentParams: ParamOnChange = p => {
+        this.plugin.state.animation.updateCurrentParams({ [p.name]: p.value });
+    }
+
+    startOrStop = () => {
+        const anim = this.plugin.state.animation;
+        if (anim.state.animationState === 'playing') anim.stop();
+        else anim.start();
+    }
+
+    render() {
+        const anim = this.plugin.state.animation;
+        if (anim.isEmpty) return null;
+
+        const isDisabled = anim.state.animationState === 'playing';
+
+        return <div className='msp-animation-section'>
+            <div className='msp-section-header'>Animations</div>
+
+            <ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} />
+            <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} />
+
+            <div className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}>
+                    {anim.state.animationState === 'playing' ? 'Stop' : 'Start'}
+                </button>
+            </div>
+        </div>
+    }
+}

+ 4 - 5
src/mol-plugin/ui/state/apply-action.tsx

@@ -6,8 +6,7 @@
 
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginContext } from 'mol-plugin/context';
-import { State, Transform } from 'mol-state';
-import { StateAction } from 'mol-state/action';
+import { State, StateTransform, StateAction } from 'mol-state';
 import { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -17,13 +16,13 @@ export { ApplyActionContol };
 namespace ApplyActionContol {
     export interface Props {
         plugin: PluginContext,
-        nodeRef: Transform.Ref,
+        nodeRef: StateTransform.Ref,
         state: State,
         action: StateAction
     }
 
     export interface ComponentState {
-        ref: Transform.Ref,
+        ref: StateTransform.Ref,
         version: string,
         params: any,
         error?: string,
@@ -48,7 +47,7 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
     applyText() { return 'Apply'; }
     isUpdate() { return false; }
 
-    private _getInfo = memoizeLatest((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
+    private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 
     state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
 

+ 23 - 15
src/mol-plugin/ui/state/common.tsx

@@ -4,18 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State, Transform, Transformer } from 'mol-state';
+import { State, StateTransform, StateTransformer, StateAction } from 'mol-state';
 import * as React from 'react';
-import { PurePluginComponent } from '../base';
+import { PurePluginUIComponent } from '../base';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
-import { StateAction } from 'mol-state/action';
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 
 export { StateTransformParameters, TransformContolBase };
 
-class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
+class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> {
     validate(params: any) {
         // TODO
         return void 0;
@@ -61,7 +60,7 @@ namespace StateTransformParameters {
         return true;
     }
 
-    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] {
+    export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: StateTransform.Ref): Props['info'] {
         const source = state.cells.get(nodeRef)!.obj!;
         const params = action.definition.params ? action.definition.params(source, plugin) : { };
         const initialValues = PD.getDefaultValues(params);
@@ -72,7 +71,7 @@ namespace StateTransformParameters {
         };
     }
 
-    export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] {
+    export function infoFromTransform(plugin: PluginContext, state: State, transform: StateTransform): Props['info'] {
         const cell = state.cells.get(transform.ref)!;
         // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
         // const create = transform.transformer.definition.params;
@@ -88,7 +87,7 @@ namespace StateTransformParameters {
 }
 
 namespace TransformContolBase {
-    export interface ControlState {
+    export interface ComponentState {
         params: any,
         error?: string,
         busy: boolean,
@@ -97,10 +96,10 @@ namespace TransformContolBase {
     }
 }
 
-abstract class TransformContolBase<P, S extends TransformContolBase.ControlState> extends PurePluginComponent<P, S> {
+abstract class TransformContolBase<P, S extends TransformContolBase.ComponentState> extends PurePluginUIComponent<P, S> {
     abstract applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
-    abstract getHeader(): Transformer.Definition['display'];
+    abstract getHeader(): StateTransformer.Definition['display'];
     abstract canApply(): boolean;
     abstract getTransformerId(): string;
     abstract canAutoApply(newParams: any): boolean;
@@ -167,7 +166,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
 
     render() {
         const info = this.getInfo();
-        if (info.isEmpty && this.isUpdate()) return null;
+        const isEmpty = info.isEmpty && this.isUpdate();
 
         const display = this.getHeader();
 
@@ -176,17 +175,26 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
             ? this.plugin.customParamEditors.get(tId)!
             : StateTransformParameters;
 
-        return <div className='msp-transform-wrapper'>
+        const wrapClass = this.isUpdate()
+            ? !isEmpty && !this.state.isCollapsed
+            ? 'msp-transform-update-wrapper'
+            : 'msp-transform-update-wrapper-collapsed'
+            : 'msp-transform-wrapper';
+
+        return <div className={wrapClass}>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button>
-                {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>}
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded} title={display.description}>
+                    {display.name}
+                    {!isEmpty && this.state.isCollapsed && this.isUpdate() && <small>Click to Edit</small>}
+                </button>
             </div>
-            {!this.state.isCollapsed && <>
+            {!isEmpty && !this.state.isCollapsed && <>
                 <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
 
                 <div className='msp-transform-apply-wrap'>
+                    <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'>↻</button>
                     <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}>
-                        ↶ Reset
+                        ↶ Back
                     </button>
                     <div className='msp-transform-apply'>
                         <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-${this.canApply() ? 'on' : 'off'}`} onClick={this.apply} disabled={!this.canApply()}>

+ 113 - 27
src/mol-plugin/ui/state-tree.tsx → src/mol-plugin/ui/state/tree.tsx

@@ -1,23 +1,49 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018 - 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
 import * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { State, StateObject } from 'mol-state'
+import { State, StateObject, StateObjectCell, StateTransform } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from '../base';
+import { UpdateTransformContol } from './update-transform';
+import { StateObjectActions } from './actions';
+import { Observable, Subject } from 'rxjs';
+
+export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
+    state = { showActions: true };
+
+    componentDidMount() {
+        this.subscribe(this.plugin.events.state.cell.created, e => {
+            if (e.cell.transform.parent === StateTransform.RootRef) this.forceUpdate();
+        });
+
+        this.subscribe(this.plugin.events.state.cell.removed, e => {
+            if (e.parent === StateTransform.RootRef) this.forceUpdate();
+        });
+    }
+
+    static getDerivedStateFromProps(props: { state: State }, state: { showActions: boolean }) {
+        const n = props.state.tree.root.ref;
+        const children = props.state.tree.children.get(n);
+        const showActions = children.size === 0;
+        if (state.showActions === showActions) return null;
+        return { showActions };
+    }
 
-export class StateTree extends PluginComponent<{ state: State }> {
     render() {
-        const n = this.props.state.tree.root.ref;
-        return <StateTreeNode state={this.props.state} nodeRef={n} />;
+        const ref = this.props.state.tree.root.ref;
+        if (this.state.showActions) {
+            return <StateObjectActions state={this.props.state} nodeRef={ref} />
+        }
+        return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />;
     }
 }
 
-class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCollapsed: boolean }> {
+class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> {
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
@@ -60,24 +86,37 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, {
     }
 
     render() {
-        if (this.props.state.cells.get(this.props.nodeRef)!.obj === StateObject.Null) return null;
+        const cell = this.props.state.cells.get(this.props.nodeRef)!;
+        if (cell.obj === StateObject.Null) return null;
 
         const cellState = this.cellState;
-
+        const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost;
         const children = this.props.state.tree.children.get(this.props.nodeRef);
-        return <div>
-            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} />
+        const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
+
+        if (!showLabel) {
+            if (children.size === 0) return null;
+            return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
+                {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+            </div>;
+        }
+
+        return <>
+            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} />
             {children.size === 0
                 ? void 0
-                : <div className='msp-tree-children' style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)}
+                : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
+                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
                 </div>
             }
-        </div>;
+        </>;
     }
 }
 
-class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCurrent: boolean, isCollapsed: boolean }> {
+class StateTreeNodeLabel extends PluginUIComponent<
+    { nodeRef: string, state: State, depth: number },
+    { state: State, isCurrent: boolean, isCollapsed: boolean, updaterCollapsed: boolean }> {
+
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
@@ -107,7 +146,8 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
     state = {
         isCurrent: this.props.state.current === this.props.nodeRef,
         isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state
+        state: this.props.state,
+        updaterCollapsed: true
     }
 
     static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) {
@@ -115,18 +155,20 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         return {
             isCurrent: props.state.current === props.nodeRef,
             isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
+            state: props.state,
+            updaterCollapsed: true
         };
     }
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
+        e.currentTarget.blur();
         PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
     }
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
@@ -153,6 +195,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         e.currentTarget.blur();
     }
 
+    private toggleUpdaterObs = new Subject();
+    toggleUpdater = (e: React.MouseEvent<HTMLAnchorElement>) => {
+        e.preventDefault();
+        e.currentTarget.blur();
+        this.toggleUpdaterObs.next();
+    }
+
     render() {
         const n = this.props.state.transforms.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
@@ -171,26 +220,63 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         } else {
             const obj = cell.obj as PluginStateObject.Any;
             const title = `${obj.label} ${obj.description ? obj.description : ''}`
-            label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>;
+            if (this.state.isCurrent) {
+                label = <><a title={title} href='#' onClick={this.toggleUpdater}><b>{obj.label}</b> {obj.description ? <small>{obj.description}</small> : void 0}</a></>;
+            } else {
+                label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label} {obj.description ? <small>{obj.description}</small> : void 0}</a></>;
+            }
         }
 
         const children = this.props.state.tree.children.get(this.props.nodeRef);
         const cellState = this.props.state.cellStates.get(this.props.nodeRef);
 
-        const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
-            <span className='msp-icon msp-icon-remove' />
-        </button>;
-
         const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
             <span className='msp-icon msp-icon-visual-visibility' />
         </button>;
 
-        return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
-            {isCurrent ? <b>{label}</b> : label}
+        const style: React.HTMLAttributes<HTMLDivElement>['style'] = {
+            marginLeft: this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`,
+            // paddingLeft: !this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`,
+            borderLeft: isCurrent || this.props.depth === 0 ? 'none' : void 0
+        }
+
+        const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={style}>
+            {label}
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
-            {remove}{visibility}
-        </div>
+            {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+                <span className='msp-icon msp-icon-remove' />
+            </button>}{visibility}
+        </div>;
+
+        if (this.state.isCurrent) {
+            return <>
+                {row}
+                <StateTreeNodeTransform {...this.props} toggleCollapsed={this.toggleUpdaterObs} />
+            </>
+        }
+
+        return row;
+    }
+}
+
+class StateTreeNodeTransform extends PluginUIComponent<{ nodeRef: string, state: State, depth: number, toggleCollapsed?: Observable<any> }> {
+    componentDidMount() {
+        // this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+        //     if (this.props.nodeRef !== ref || this.props.state !== state) return;
+        //     this.forceUpdate();
+        // });
+    }
+
+    render() {
+        const ref = this.props.nodeRef;
+        const cell = this.props.state.cells.get(ref)!;
+        const parent: StateObjectCell | undefined = (cell.sourceRef && this.props.state.cells.get(cell.sourceRef)!) || void 0;
+
+        if (!parent || parent.status !== 'ok') return null;
+
+        const transform = cell.transform;
+        return <UpdateTransformContol state={this.props.state} transform={transform} initiallyCollapsed={true} toggleCollapsed={this.props.toggleCollapsed} />;
     }
 }

+ 24 - 11
src/mol-plugin/ui/state/update-transform.tsx

@@ -4,24 +4,23 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State, Transform } from 'mol-state';
+import { State, StateTransform } from 'mol-state';
 import { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
+import { Observable } from 'rxjs';
 
 export { UpdateTransformContol };
 
 namespace UpdateTransformContol {
     export interface Props {
-        transform: Transform,
-        state: State
+        transform: StateTransform,
+        state: State,
+        toggleCollapsed?: Observable<any>,
+        initiallyCollapsed?: boolean
     }
 
-    export interface ComponentState {
-        transform: Transform,
-        params: any,
-        error?: string,
-        busy: boolean,
-        isInitial: boolean
+    export interface ComponentState extends TransformContolBase.ComponentState {
+        transform: StateTransform
     }
 }
 
@@ -46,9 +45,23 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr
         return autoUpdate({ a: cell.obj!, b: parentCell.obj!, oldParams: this.getInfo().initialValues, newParams }, this.plugin);
     }
 
-    private _getInfo = memoizeLatest((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
+    componentDidMount() {
+        if (super.componentDidMount) super.componentDidMount();
 
-    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
+        if (this.props.toggleCollapsed) this.subscribe(this.props.toggleCollapsed, () => this.setState({ isCollapsed: !this.state.isCollapsed }));
+
+        this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
+            if (this.props.transform.ref !== ref || this.props.state !== state) return;
+            if (this.state.params !== this.props.transform.params) {
+                this._getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t));
+                this.setState({ params: this.props.transform.params, isInitial: true })
+            }
+        });
+    }
+
+    private _getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t));
+
+    state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false, isCollapsed: this.props.initiallyCollapsed };
 
     static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) {
         if (props.transform === state.transform) return null;

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

@@ -5,13 +5,13 @@
  */
 
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { OrderedMap } from 'immutable';
 import { TaskManager } from 'mol-plugin/util/task-manager';
 import { filter } from 'rxjs/operators';
 import { Progress } from 'mol-task';
 
-export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> {
+export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> {
     componentDidMount() {
         this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => {
             this.setState({ tracked: this.state.tracked.set(e.id, e) })
@@ -30,7 +30,7 @@ export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: Orde
     }
 }
 
-class ProgressEntry extends PluginComponent<{ event: TaskManager.ProgressEvent }> {
+class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> {
     render() {
         const root = this.props.event.progress.root;
         const subtaskCount = countSubtasks(this.props.event.progress.root) - 1;

+ 10 - 10
src/mol-plugin/ui/viewport.tsx

@@ -8,7 +8,7 @@
 import * as React from 'react';
 import { ButtonsType } from 'mol-util/input/input-observer';
 import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { PluginCommands } from 'mol-plugin/command';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
@@ -20,7 +20,7 @@ interface ViewportState {
     noWebGl: boolean
 }
 
-export class ViewportControls extends PluginComponent {
+export class ViewportControls extends PluginUIComponent {
     state = {
         isSettingsExpanded: false
     }
@@ -35,11 +35,11 @@ export class ViewportControls extends PluginComponent {
     }
 
     toggleControls = () => {
-        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.latestState.showControls } });
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.state.showControls } });
     }
 
     toggleExpanded = () => {
-        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.latestState.isExpanded } });
+        PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } });
     }
 
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
@@ -55,7 +55,7 @@ export class ViewportControls extends PluginComponent {
             this.forceUpdate();
         });
 
-        this.subscribe(this.plugin.layout.updated, () => {
+        this.subscribe(this.plugin.layout.events.updated, () => {
             this.forceUpdate();
         });
     }
@@ -73,15 +73,15 @@ export class ViewportControls extends PluginComponent {
         // TODO: show some icons dimmed etc..
         return <div className={'msp-viewport-controls'}>
             <div className='msp-viewport-controls-buttons'>
-                {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('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)}
+                {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.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'>
                 <ControlGroup header='Layout' initialExpanded={true}>
-                    <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} />
+                    <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} />
                 </ControlGroup>
                 <ControlGroup header='Viewport' initialExpanded={true}>
                     <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
@@ -91,7 +91,7 @@ export class ViewportControls extends PluginComponent {
     }
 }
 
-export class Viewport extends PluginComponent<{ }, ViewportState> {
+export class Viewport extends PluginUIComponent<{ }, ViewportState> {
     private container = React.createRef<HTMLDivElement>();
     private canvas = React.createRef<HTMLCanvasElement>();
 
@@ -128,7 +128,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
             idHelper.select(x, y);
         });
 
-        this.subscribe(this.plugin.layout.updated, () => {
+        this.subscribe(this.plugin.layout.events.updated, () => {
             setTimeout(this.handleResize, 50);
         });
     }

+ 1 - 1
src/mol-plugin/util/canvas3d-identify.ts

@@ -52,7 +52,7 @@ export class Canvas3dIdentifyHelper {
     }
 
     private animate: (t: number) => void = t => {
-        if (this.inside && t - this.prevT > 1000 / this.maxFps) {
+        if (!this.ctx.state.animation.isAnimating && this.inside && t - this.prevT > 1000 / this.maxFps) {
             this.prevT = t;
             this.currentIdentifyT = t;
             this.identify(false, t);

+ 1 - 5
src/mol-repr/representation.ts

@@ -188,11 +188,7 @@ namespace Representation {
                 }
                 return renderObjects
             },
-            get props() {
-                const props = {}
-                reprList.forEach(r => Object.assign(props, r.props))
-                return props as P
-            },
+            get props() { return currentProps },
             get params() { return currentParams },
             createOrUpdate: (props: Partial<P> = {}, data?: D) => {
                 if (data && data !== currentData) {

+ 33 - 27
src/mol-repr/volume/isosurface.ts

@@ -19,23 +19,27 @@ import { VisualContext } from 'mol-repr/visual';
 import { NullLocation } from 'mol-model/location';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 
-const IsoValueParam = PD.Conditioned(
-    VolumeIsoValue.relative(VolumeData.Empty.dataStats, 2),
-    {
-        'absolute': PD.Converted(
-            (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v).absoluteValue,
-            (v: number) => VolumeIsoValue.absolute(VolumeData.Empty.dataStats, v),
-            PD.Numeric(0.5, { min: -1, max: 1, step: 0.01 })
-        ),
-        'relative': PD.Converted(
-            (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v).relativeValue,
-            (v: number) => VolumeIsoValue.relative(VolumeData.Empty.dataStats, v),
-            PD.Numeric(2, { min: -10, max: 10, step: 0.01 })
-        )
-    },
-    (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
-    (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v) : VolumeIsoValue.toRelative(v)
-)
+export function createIsoValueParam(defaultValue: VolumeIsoValue) {
+    return PD.Conditioned(
+        defaultValue,
+        {
+            'absolute': PD.Converted(
+                (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats).absoluteValue,
+                (v: number) => VolumeIsoValue.absolute(v),
+                PD.Numeric(0.5, { min: -1, max: 1, step: 0.01 })
+            ),
+            'relative': PD.Converted(
+                (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, VolumeData.One.dataStats).relativeValue,
+                (v: number) => VolumeIsoValue.relative(v),
+                PD.Numeric(2, { min: -10, max: 10, step: 0.01 })
+            )
+        },
+        (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
+        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats) : VolumeIsoValue.toRelative(v, VolumeData.One.dataStats)
+    )
+}
+
+export const IsoValueParam = createIsoValueParam(VolumeIsoValue.relative(2));
 type IsoValueParam = typeof IsoValueParam
 
 export const VolumeIsosurfaceParams = {
@@ -50,7 +54,7 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
     ctx.runtime.update({ message: 'Marching cubes...' });
 
     const surface = await computeMarchingCubesMesh({
-        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue,
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
         scalarField: volume.data
     }, mesh).runAsChild(ctx.runtime);
 
@@ -88,7 +92,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
     ctx.runtime.update({ message: 'Marching cubes...' });
 
     const wireframe = await computeMarchingCubesLines({
-        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue,
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
         scalarField: volume.data
     }, lines).runAsChild(ctx.runtime)
 
@@ -135,24 +139,26 @@ export const IsosurfaceParams = {
 export type IsosurfaceParams = typeof IsosurfaceParams
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
     const p = PD.clone(IsosurfaceParams)
-    const { min, max, mean, sigma } = volume.dataStats
+    const stats = volume.dataStats
+    const { min, max, mean, sigma } = stats
     p.isoValue = PD.Conditioned(
-        VolumeIsoValue.relative(volume.dataStats, 2),
+        VolumeIsoValue.relative(2),
         {
             'absolute': PD.Converted(
-                (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v).absoluteValue,
-                (v: number) => VolumeIsoValue.absolute(volume.dataStats, v),
+                (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, stats).absoluteValue,
+                (v: number) => VolumeIsoValue.absolute(v),
                 PD.Numeric(mean, { min, max, step: sigma / 100 })
             ),
             'relative': PD.Converted(
-                (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v).relativeValue,
-                (v: number) => VolumeIsoValue.relative(volume.dataStats, v),
-                PD.Numeric(2, { min: -10, max: 10, step: 0.001 })
+                (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, stats).relativeValue,
+                (v: number) => VolumeIsoValue.relative(v),
+                PD.Numeric(2, { min: Math.floor((min - mean) / sigma), max: Math.ceil((max - mean) / sigma), step: Math.ceil((max - min) / sigma) / 100 })
             )
         },
         (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
-        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v) : VolumeIsoValue.toRelative(v)
+        (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, stats) : VolumeIsoValue.toRelative(v, stats)
     )
+
     return p
 }
 

+ 12 - 6
src/mol-state/action.ts

@@ -9,7 +9,8 @@ import { UUID } from 'mol-util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
-import { Transformer } from './transformer';
+import { StateTransformer } from './transformer';
+import { StateTransform } from './transform';
 
 export { StateAction };
 
@@ -45,7 +46,7 @@ namespace StateAction {
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
         /** Test if the transform can be applied to a given node */
-        isApplicable?(a: A, globalCtx: unknown): boolean
+        isApplicable?(a: A, aTransform: StateTransform<any, A, any>, globalCtx: unknown): boolean
     }
 
     export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
@@ -63,12 +64,15 @@ namespace StateAction {
         return action;
     }
 
-    export function fromTransformer<T extends Transformer>(transformer: T) {
+    export function fromTransformer<T extends StateTransformer>(transformer: T) {
         const def = transformer.definition;
-        return create<Transformer.From<T>, void, Transformer.Params<T>>({
+        return create<StateTransformer.From<T>, void, StateTransformer.Params<T>>({
             from: def.from,
             display: def.display,
-            params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'],
+            params: def.params as StateTransformer.Definition<StateTransformer.From<T>, any, StateTransformer.Params<T>>['params'],
+            isApplicable: transformer.definition.isApplicable
+                ? (a, t, ctx) => transformer.definition.isApplicable!(a, ctx)
+                : void 0,
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 return state.updateTree(tree) as Task<void>;
@@ -80,7 +84,8 @@ namespace StateAction {
         export interface Type<A extends StateObject.Ctor, P extends { }> {
             from?: A | A[],
             params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
-            display?: string | { name: string, description?: string }
+            display?: string | { name: string, description?: string },
+            isApplicable?: DefinitionBase<StateObject.From<A>, any, P>['isApplicable']
         }
 
         export interface Root {
@@ -106,6 +111,7 @@ namespace StateAction {
                     : !!info.params
                     ? info.params as any
                     : void 0,
+                isApplicable: info.isApplicable,
                 ...(typeof def === 'function'
                     ? { run: def }
                     : def)

+ 31 - 7
src/mol-state/action/manager.ts

@@ -4,9 +4,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateAction } from 'mol-state/action';
-import { StateObject } from '../object';
-import { Transformer } from 'mol-state/transformer';
+import { StateAction } from '../action';
+import { StateObject, StateObjectCell } from '../object';
+import { StateTransformer } from '../transformer';
 
 export { StateActionManager }
 
@@ -14,8 +14,8 @@ class StateActionManager {
     private actions: Map<StateAction['id'], StateAction> = new Map();
     private fromTypeIndex = new Map<StateObject.Type, StateAction[]>();
 
-    add(actionOrTransformer: StateAction | Transformer) {
-        const action = Transformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer;
+    add(actionOrTransformer: StateAction | StateTransformer) {
+        const action = StateTransformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer;
 
         if (this.actions.has(action.id)) return this;
 
@@ -32,7 +32,31 @@ class StateActionManager {
         return this;
     }
 
-    fromType(type: StateObject.Type): ReadonlyArray<StateAction> {
-        return this.fromTypeIndex.get(type) || [];
+    fromCell(cell: StateObjectCell, ctx: unknown): ReadonlyArray<StateAction> {
+        const obj = cell.obj;
+        if (!obj) return [];
+
+        const actions = this.fromTypeIndex.get(obj.type);
+        if (!actions) return [];
+        let hasTest = false;
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                hasTest = true;
+                break;
+            }
+        }
+        if (!hasTest) return actions;
+
+        const ret: StateAction[] = [];
+        for (const a of actions) {
+            if (a.definition.isApplicable) {
+                if (a.definition.isApplicable(obj, cell.transform, ctx)) {
+                    ret.push(a);
+                }
+            } else {
+                ret.push(a);
+            }
+        }
+        return ret;
     }
 }

+ 5 - 2
src/mol-state/index.ts

@@ -5,7 +5,10 @@
  */
 
 export * from './object'
+export * from './tree'
 export * from './state'
+export * from './state/builder'
+export * from './state/selection'
 export * from './transformer'
-export * from './tree'
-export * from './transform'
+export * from './transform'
+export * from './action'

+ 6 - 7
src/mol-state/object.ts

@@ -5,10 +5,10 @@
  */
 
 import { UUID } from 'mol-util';
-import { Transform } from './transform';
+import { StateTransform } from './transform';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { State } from './state';
-import { StateSelection } from './state/selection';
+import { StateSelection } from 'mol-state';
 
 export { StateObject, StateObjectCell }
 
@@ -53,13 +53,12 @@ namespace StateObject {
     };
 }
 
-interface StateObjectCell {
-    transform: Transform,
+interface StateObjectCell<T = StateObject> {
+    transform: StateTransform,
 
     // Which object was used as a parent to create data in this cell
-    sourceRef: Transform.Ref | undefined,
+    sourceRef: StateTransform.Ref | undefined,
 
-    version: string
     status: StateObjectCell.Status,
 
     params: {
@@ -68,7 +67,7 @@ interface StateObjectCell {
     } | undefined;
 
     errorText?: string,
-    obj?: StateObject
+    obj?: T
 }
 
 namespace StateObjectCell {

+ 47 - 54
src/mol-state/state.ts

@@ -6,13 +6,12 @@
 
 import { StateObject, StateObjectCell } from './object';
 import { StateTree } from './tree';
-import { Transform } from './transform';
-import { Transformer } from './transformer';
-import { UUID } from 'mol-util';
+import { StateTransform } from './transform';
+import { StateTransformer } from './transformer';
 import { RuntimeContext, Task } from 'mol-task';
 import { StateSelection } from './state/selection';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
-import { StateTreeBuilder } from './tree/builder';
+import { StateBuilder } from './state/builder';
 import { StateAction } from './action';
 import { StateActionManager } from './action/manager';
 import { TransientTree } from './tree/transient';
@@ -23,10 +22,10 @@ import { ParamDefinition } from 'mol-util/param-definition';
 export { State }
 
 class State {
-    private _tree: TransientTree = StateTree.createEmpty().asTransient();
+    private _tree: TransientTree;
 
     protected errorFree = true;
-    private transformCache = new Map<Transform.Ref, unknown>();
+    private transformCache = new Map<StateTransform.Ref, unknown>();
 
     private ev = RxEventHelper.create();
 
@@ -35,7 +34,7 @@ class State {
         cell: {
             stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
-            removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(),
+            removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
         object: {
             updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
@@ -47,7 +46,7 @@ class State {
     };
 
     readonly behaviors = {
-        currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef })
+        currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: StateTransform.RootRef })
     };
 
     readonly actions = new StateActionManager();
@@ -57,7 +56,7 @@ class State {
     get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
 
-    build() { return this._tree.build(); }
+    build() { return new StateBuilder.Root(this._tree); }
 
     readonly cells: State.Cells = new Map();
 
@@ -70,11 +69,11 @@ class State {
         return this.updateTree(tree);
     }
 
-    setCurrent(ref: Transform.Ref) {
+    setCurrent(ref: StateTransform.Ref) {
         this.behaviors.currentObject.next({ state: this, ref });
     }
 
-    updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
+    updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
         const update = typeof stateOrProvider === 'function'
             ? stateOrProvider(this.tree.cellStates.get(ref))
             : stateOrProvider;
@@ -110,7 +109,7 @@ class State {
      * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
      * If no ref is specified, apply to root.
      */
-    applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
+    applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: StateTransform.Ref = StateTransform.RootRef): Task<void> {
         return Task.create('Apply Action', ctx => {
             const cell = this.cells.get(ref);
             if (!cell) throw new Error(`'${ref}' does not exist.`);
@@ -123,19 +122,19 @@ class State {
     /**
      * Reconcialites the existing state tree with the new version.
      *
-     * If the tree is StateTreeBuilder.To<T>, the corresponding StateObject is returned by the task.
+     * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task.
      * @param tree Tree instance or a tree builder instance
      * @param doNotReportTiming Indicates whether to log timing of the individual transforms
      */
-    updateTree<T extends StateObject>(tree: StateTree | StateTreeBuilder | StateTreeBuilder.To<T>, doNotLogTiming?: boolean): Task<T>
-    updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming?: boolean): Task<void>
-    updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming: boolean = false): Task<any> {
+    updateTree<T extends StateObject>(tree: StateTree | StateBuilder | StateBuilder.To<T>, doNotLogTiming?: boolean): Task<T>
+    updateTree(tree: StateTree | StateBuilder, doNotLogTiming?: boolean): Task<void>
+    updateTree(tree: StateTree | StateBuilder, doNotLogTiming: boolean = false): Task<any> {
         return Task.create('Update Tree', async taskCtx => {
             let updated = false;
             try {
                 const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, doNotLogTiming);
                 updated = await update(ctx);
-                if (StateTreeBuilder.isTo(tree)) {
+                if (StateBuilder.isTo(tree)) {
                     const cell = this.select(tree.ref)[0];
                     return cell && cell.obj;
                 }
@@ -145,20 +144,20 @@ class State {
         });
     }
 
-    private updateTreeAndCreateCtx(tree: StateTree | StateTreeBuilder, taskCtx: RuntimeContext, doNotLogTiming: boolean) {
-        const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
+    private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, doNotLogTiming: boolean) {
+        const _tree = (StateBuilder.is(tree) ? tree.getTree() : tree).asTransient();
         const oldTree = this._tree;
         this._tree = _tree;
 
         const ctx: UpdateContext = {
             parent: this,
-            editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
+            editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0,
 
             errorFree: this.errorFree,
             taskCtx,
             oldTree,
             tree: _tree,
-            cells: this.cells as Map<Transform.Ref, StateObjectCell>,
+            cells: this.cells as Map<StateTransform.Ref, StateObjectCell>,
             transformCache: this.transformCache,
 
             results: [],
@@ -175,16 +174,16 @@ class State {
         return ctx;
     }
 
-    constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
+    constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
+        this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient();
         const tree = this._tree;
         const root = tree.root;
 
-        (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, {
+        (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
             transform: root,
             sourceRef: void 0,
             obj: rootObject,
             status: 'ok',
-            version: root.version,
             errorText: void 0,
             params: {
                 definition: {},
@@ -197,10 +196,10 @@ class State {
 }
 
 namespace State {
-    export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell>
+    export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell>
 
     export type Tree = StateTree
-    export type Builder = StateTreeBuilder
+    export type Builder = StateBuilder
 
     export interface ObjectEvent {
         state: State,
@@ -211,22 +210,22 @@ namespace State {
         readonly tree: StateTree.Serialized
     }
 
-    export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) {
         return new State(rootObject, params);
     }
 }
 
-type Ref = Transform.Ref
+type Ref = StateTransform.Ref
 
 interface UpdateContext {
     parent: State,
-    editInfo: StateTreeBuilder.EditInfo | undefined
+    editInfo: StateBuilder.EditInfo | undefined
 
     errorFree: boolean,
     taskCtx: RuntimeContext,
     oldTree: StateTree,
     tree: TransientTree,
-    cells: Map<Transform.Ref, StateObjectCell>,
+    cells: Map<StateTransform.Ref, StateObjectCell>,
     transformCache: Map<Ref, unknown>,
 
     results: UpdateNodeResult[],
@@ -243,7 +242,7 @@ async function update(ctx: UpdateContext) {
     // if only a single node was added/updated, we can skip potentially expensive diffing
     const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree);
 
-    let deletes: Transform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: Transform.Ref[];
+    let deletes: StateTransform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: StateTransform.Ref[];
 
     if (fastTrack) {
         deletes = [];
@@ -309,7 +308,7 @@ async function update(ctx: UpdateContext) {
         await updateSubtree(ctx, root);
     }
 
-    let newCurrent: Transform.Ref | undefined = ctx.newCurrent;
+    let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
     // Raise object updated events
     for (const update of ctx.results) {
         if (update.action === 'created') {
@@ -342,15 +341,15 @@ async function update(ctx: UpdateContext) {
     return deletes.length > 0 || roots.length > 0 || ctx.changed;
 }
 
-function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) {
+function findUpdateRoots(cells: Map<StateTransform.Ref, StateObjectCell>, tree: StateTree) {
     const findState = { roots: [] as Ref[], cells };
     StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
     return findState.roots;
 }
 
-function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
+function findUpdateRootsVisitor(n: StateTransform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
     const cell = s.cells.get(n.ref);
-    if (!cell || cell.version !== n.version || cell.status === 'error') {
+    if (!cell || cell.transform.version !== n.version || cell.status === 'error') {
         s.roots.push(n.ref);
         return false;
     }
@@ -360,7 +359,7 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells:
 }
 
 type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
-function checkDeleteVisitor(n: Transform, _: any, ctx: FindDeletesCtx) {
+function checkDeleteVisitor(n: StateTransform, _: any, ctx: FindDeletesCtx) {
     if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
 }
 function findDeletes(ctx: UpdateContext): Ref[] {
@@ -369,7 +368,7 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
 }
 
-function syncStatesVisitor(n: Transform, tree: StateTree, oldState: StateTree.CellStates) {
+function syncStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) {
     if (!oldState.has(n.ref)) return;
     (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
 }
@@ -385,7 +384,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta
     if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
 }
 
-function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) {
+function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
     ctx.cells.get(t.ref)!.transform = t;
     setCellStatus(ctx, t.ref, 'pending');
 }
@@ -397,7 +396,7 @@ function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
 }
 
 type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] }
-function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCellsCtx) {
+function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: InitCellsCtx) {
     if (ctx.cells.has(transform.ref)) {
         return;
     }
@@ -406,7 +405,6 @@ function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCell
         transform,
         sourceRef: void 0,
         status: 'pending',
-        version: UUID.create22(),
         errorText: void 0,
         params: void 0
     };
@@ -428,7 +426,7 @@ function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<
 }
 
 function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref {
-    if (ref === Transform.RootRef) return ref;
+    if (ref === StateTransform.RootRef) return ref;
 
     const node = tree.transforms.get(ref)!;
     const siblings = tree.children.get(node.parent)!.values();
@@ -542,7 +540,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
     }
 }
 
-function resolveParams(ctx: UpdateContext, transform: Transform, src: StateObject) {
+function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) {
     const prms = transform.transformer.definition.params;
     const definition = prms ? prms(src, ctx.parent.globalContext) : {};
     const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
@@ -555,8 +553,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
     const transform = current.transform;
 
     // special case for Root
-    if (current.transform.ref === Transform.RootRef) {
-        current.version = transform.version;
+    if (current.transform.ref === StateTransform.RootRef) {
         return { action: 'none' };
     }
 
@@ -574,7 +571,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
         current.params = params;
         const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values);
         current.obj = obj;
-        current.version = transform.version;
 
         return { ref: currentRef, action: 'created', obj };
     } else {
@@ -584,21 +580,18 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
         const updateKind = !!current.obj && current.obj !== StateObject.Null
             ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams)
-            : Transformer.UpdateResult.Recreate;
+            : StateTransformer.UpdateResult.Recreate;
 
         switch (updateKind) {
-            case Transformer.UpdateResult.Recreate: {
+            case StateTransformer.UpdateResult.Recreate: {
                 const oldObj = current.obj;
                 const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams);
                 current.obj = newObj;
-                current.version = transform.version;
                 return { ref: currentRef, action: 'replaced', oldObj, obj: newObj };
             }
-            case Transformer.UpdateResult.Updated:
-                current.version = transform.version;
+            case StateTransformer.UpdateResult.Updated:
                 return { ref: currentRef, action: 'updated', obj: current.obj! };
             default:
-                current.version = transform.version;
                 return { action: 'none' };
         }
     }
@@ -609,15 +602,15 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
     return t as T;
 }
 
-function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
+function createObject(ctx: UpdateContext, ref: Ref, transformer: StateTransformer, a: StateObject, params: any) {
     const cache = Object.create(null);
     ctx.transformCache.set(ref, cache);
     return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx);
 }
 
-async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
+async function updateObject(ctx: UpdateContext, ref: Ref, transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
     if (!transformer.definition.update) {
-        return Transformer.UpdateResult.Recreate;
+        return StateTransformer.UpdateResult.Recreate;
     }
     let cache = ctx.transformCache.get(ref);
     if (!cache) {

+ 125 - 0
src/mol-state/state/builder.ts

@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTree } from '../tree/immutable';
+import { TransientTree } from '../tree/transient';
+import { StateObject, StateObjectCell } from '../object';
+import { StateTransform } from '../transform';
+import { StateTransformer } from '../transformer';
+
+export { StateBuilder }
+
+interface StateBuilder {
+    readonly editInfo: StateBuilder.EditInfo,
+    getTree(): StateTree
+}
+
+namespace StateBuilder {
+    export interface EditInfo {
+        sourceTree: StateTree,
+        count: number,
+        lastUpdate?: StateTransform.Ref
+    }
+
+    interface State {
+        tree: TransientTree,
+        editInfo: EditInfo
+    }
+
+    export function is(obj: any): obj is StateBuilder {
+        return !!obj && typeof (obj as StateBuilder).getTree === 'function';
+    }
+
+    export function isTo(obj: any): obj is StateBuilder.To<any> {
+        return !!obj && typeof (obj as StateBuilder).getTree === 'function' && typeof (obj as StateBuilder.To<any>).ref === 'string';
+    }
+
+    export class Root implements StateBuilder {
+        private state: State;
+        get editInfo() { return this.state.editInfo; }
+
+        to<A extends StateObject>(ref: StateTransform.Ref) { return new To<A>(this.state, ref, this); }
+        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
+        delete(ref: StateTransform.Ref) {
+            this.editInfo.count++;
+            this.state.tree.remove(ref);
+            return this;
+        }
+        getTree(): StateTree { return this.state.tree.asImmutable(); }
+        constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
+    }
+
+    export class To<A extends StateObject> implements StateBuilder {
+        get editInfo() { return this.state.editInfo; }
+
+        readonly ref: StateTransform.Ref;
+
+        /**
+         * Apply the transformed to the parent node
+         * If no params are specified (params <- undefined), default params are lazily resolved.
+         */
+        apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+            const t = tr.apply(this.ref, params, options);
+            this.state.tree.add(t, initialCellState);
+            this.editInfo.count++;
+            this.editInfo.lastUpdate = t.ref;
+            return new To(this.state, t.ref, this.root);
+        }
+
+        /**
+         * Inserts a new transform that does not change the object type and move the original children to it.
+         */
+        insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> {
+            // cache the children
+            const children = this.state.tree.children.get(this.ref).toArray();
+
+            // add the new node
+            const t = tr.apply(this.ref, params, options);
+            this.state.tree.add(t, initialCellState);
+
+            // move the original children to the new node
+            for (const c of children) {
+                this.state.tree.changeParent(c, t.ref);
+            }
+
+            this.editInfo.count++;
+            this.editInfo.lastUpdate = t.ref;
+            return new To(this.state, t.ref, this.root);
+        }
+
+        update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>): Root
+        update(params: any): Root
+        update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) {
+            let params: any;
+            if (provider) {
+                const old = this.state.tree.transforms.get(this.ref)!;
+                params = provider(old.params as any);
+            } else {
+                params = paramsOrTransformer;
+            }
+
+            if (this.state.tree.setParams(this.ref, params)) {
+                this.editInfo.count++;
+                this.editInfo.lastUpdate = this.ref;
+            }
+
+            return this.root;
+        }
+
+        to<A extends StateObject>(ref: StateTransform.Ref) { return this.root.to<A>(ref); }
+        toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
+        delete(ref: StateTransform.Ref) { return this.root.delete(ref); }
+
+        getTree(): StateTree { return this.state.tree.asImmutable(); }
+
+        constructor(private state: State, ref: StateTransform.Ref, private root: Root) {
+            this.ref = ref;
+            if (!this.state.tree.transforms.has(ref)) {
+                throw new Error(`Could not find node '${ref}'.`);
+            }
+        }
+    }
+}

+ 7 - 7
src/mol-state/state/selection.ts

@@ -7,7 +7,7 @@
 import { StateObject, StateObjectCell } from '../object';
 import { State } from '../state';
 import { StateTree } from '../tree';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 
 namespace StateSelection {
     export type Selector = Query | Builder | string | StateObjectCell;
@@ -29,7 +29,7 @@ namespace StateSelection {
     }
 
     function isObj(arg: any): arg is StateObjectCell {
-        return (arg as StateObjectCell).version !== void 0;
+        return (arg as StateObjectCell).transform !== void 0 && (arg as StateObjectCell).status !== void 0;
     }
 
     function isBuilder(arg: any): arg is Builder {
@@ -75,7 +75,7 @@ namespace StateSelection {
     export namespace Generators {
         export const root = build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]);
 
-        export function byRef(...refs: Transform.Ref[]) {
+        export function byRef(...refs: StateTransform.Ref[]) {
             return build(() => (state: State) => {
                 const ret: StateObjectCell[] = [];
                 for (const ref of refs) {
@@ -97,7 +97,7 @@ namespace StateSelection {
             });
         }
 
-        function _findRootsOfType(n: Transform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) {
+        function _findRootsOfType(n: StateTransform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) {
             const cell = s.cells.get(n.ref);
             if (cell && cell.obj && cell.obj.type === s.type) {
                 s.roots.push(cell);
@@ -196,7 +196,7 @@ namespace StateSelection {
     registerModifier('parent', parent);
     export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); }
 
-    export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
+    export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
         let current = tree.transforms.get(root)!, len = types.length;
         while (true) {
             current = tree.transforms.get(current.parent)!;
@@ -206,13 +206,13 @@ namespace StateSelection {
             for (let i = 0; i < len; i++) {
                 if (obj.type === types[i].type) return cells.get(current.ref);
             }
-            if (current.ref === Transform.RootRef) {
+            if (current.ref === StateTransform.RootRef) {
                 return void 0;
             }
         }
     }
 
-    export function findRootOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
+    export function findRootOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
         let parent: StateObjectCell | undefined, _root = root;
         while (true) {
             const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types);

+ 21 - 10
src/mol-state/transform.ts

@@ -5,19 +5,21 @@
  */
 
 import { StateObject } from './object';
-import { Transformer } from './transformer';
+import { StateTransformer } from './transformer';
 import { UUID } from 'mol-util';
 
-export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
+export { Transform as StateTransform }
+
+interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
     readonly parent: Transform.Ref,
-    readonly transformer: Transformer<A, B, P>,
+    readonly transformer: StateTransformer<A, B, P>,
     readonly props: Transform.Props,
     readonly ref: Transform.Ref,
     readonly params?: P,
     readonly version: string
 }
 
-export namespace Transform {
+namespace Transform {
     export type Ref = string
 
     export const RootRef = '-=root=-' as Ref;
@@ -25,7 +27,8 @@ export namespace Transform {
     export interface Props {
         tag?: string
         isGhost?: boolean,
-        isBinding?: boolean
+        // determine if the corresponding cell can be deleted by the user.
+        isLocked?: boolean
     }
 
     export interface Options {
@@ -33,7 +36,7 @@ export namespace Transform {
         props?: Props
     }
 
-    export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
+    export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: StateTransformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
         return {
             parent,
@@ -45,12 +48,20 @@ export namespace Transform {
         }
     }
 
-    export function withParams<T>(t: Transform, params: any): Transform {
+    export function withParams(t: Transform, params: any): Transform {
         return { ...t, params, version: UUID.create22() };
     }
 
-    export function createRoot(): Transform {
-        return create(RootRef, Transformer.ROOT, {}, { ref: RootRef });
+    export function withParent(t: Transform, parent: Ref): Transform {
+        return { ...t, parent, version: UUID.create22() };
+    }
+
+    export function withNewVersion(t: Transform): Transform {
+        return { ...t, version: UUID.create22() };
+    }
+
+    export function createRoot(props?: Props): Transform {
+        return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props });
     }
 
     export interface Serialized {
@@ -78,7 +89,7 @@ export namespace Transform {
     }
 
     export function fromJSON(t: Serialized): Transform {
-        const transformer = Transformer.get(t.transformer);
+        const transformer = StateTransformer.get(t.transformer);
         const pFromJson = transformer.definition.customSerialization
             ? transformer.definition.customSerialization.toJSON
             : _id;

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

@@ -5,25 +5,28 @@
  */
 
 import { Task } from 'mol-task';
-import { StateObject } from './object';
-import { Transform } from './transform';
+import { StateObject, StateObjectCell } from './object';
+import { StateTransform } from './transform';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateAction } from './action';
 import { capitalize } from 'mol-util/string';
 
-export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
-    apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
+export { Transformer as StateTransformer }
+
+interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
+    apply(parent: StateTransform.Ref, params?: P, props?: Partial<StateTransform.Options>): StateTransform<A, B, P>,
     toAction(): StateAction<A, void, P>,
     readonly namespace: string,
     readonly id: Transformer.Id,
     readonly definition: Transformer.Definition<A, B, P>
 }
 
-export namespace Transformer {
+namespace Transformer {
     export type Id = string & { '@type': 'transformer-id' }
     export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
     export type From<T extends Transformer<any, any, any>> = T extends Transformer<infer A, any, any> ? A : unknown;
     export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
+    export type Cell<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? StateObjectCell<B> : unknown;
 
     export function is(obj: any): obj is Transformer {
         return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function';
@@ -130,7 +133,7 @@ export namespace Transformer {
         }
 
         const t: Transformer<A, B, P> = {
-            apply(parent, params, props) { return Transform.create<A, B, P>(parent, t, params, props); },
+            apply(parent, params, props) { return StateTransform.create<A, B, P>(parent, t, params, props); },
             toAction() { return StateAction.fromTransformer(t); },
             namespace,
             id,

+ 0 - 104
src/mol-state/tree/builder.ts

@@ -1,104 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { StateTree } from './immutable';
-import { TransientTree } from './transient';
-import { StateObject, StateObjectCell } from '../object';
-import { Transform } from '../transform';
-import { Transformer } from '../transformer';
-
-export { StateTreeBuilder }
-
-interface StateTreeBuilder {
-    readonly editInfo: StateTreeBuilder.EditInfo,
-    getTree(): StateTree
-}
-
-namespace StateTreeBuilder {
-    export interface EditInfo {
-        sourceTree: StateTree,
-        count: number,
-        lastUpdate?: Transform.Ref
-    }
-
-    interface State {
-        tree: TransientTree,
-        editInfo: EditInfo
-    }
-
-    export function is(obj: any): obj is StateTreeBuilder {
-        return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function';
-    }
-
-    export function isTo(obj: any): obj is StateTreeBuilder.To<any> {
-        return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function' && typeof (obj as StateTreeBuilder.To<any>).ref === 'string';
-    }
-
-    export class Root implements StateTreeBuilder {
-        private state: State;
-        get editInfo() { return this.state.editInfo; }
-
-        to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
-        toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
-        delete(ref: Transform.Ref) {
-            this.editInfo.count++;
-            this.state.tree.remove(ref);
-            return this;
-        }
-        getTree(): StateTree { return this.state.tree.asImmutable(); }
-        constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
-    }
-
-    export class To<A extends StateObject> implements StateTreeBuilder {
-        get editInfo() { return this.state.editInfo; }
-
-        readonly ref: Transform.Ref;
-
-        /**
-         * Apply the transformed to the parent node
-         * If no params are specified (params <- undefined), default params are lazily resolved.
-         */
-        apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, options?: Partial<Transform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<Transformer.To<T>> {
-            const t = tr.apply(this.ref, params, options);
-            this.state.tree.add(t, initialCellState);
-            this.editInfo.count++;
-            this.editInfo.lastUpdate = t.ref;
-            return new To(this.state, t.ref, this.root);
-        }
-
-        update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root
-        update(params: any): Root
-        update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) {
-            let params: any;
-            if (provider) {
-                const old = this.state.tree.transforms.get(this.ref)!;
-                params = provider(old.params as any);
-            } else {
-                params = paramsOrTransformer;
-            }
-
-            if (this.state.tree.setParams(this.ref, params)) {
-                this.editInfo.count++;
-                this.editInfo.lastUpdate = this.ref;
-            }
-
-            return this.root;
-        }
-
-        to<A extends StateObject>(ref: Transform.Ref) { return this.root.to<A>(ref); }
-        toRoot<A extends StateObject>() { return this.root.toRoot<A>(); }
-        delete(ref: Transform.Ref) { return this.root.delete(ref); }
-
-        getTree(): StateTree { return this.state.tree.asImmutable(); }
-
-        constructor(private state: State, ref: Transform.Ref, private root: Root) {
-            this.ref = ref;
-            if (!this.state.tree.transforms.has(ref)) {
-                throw new Error(`Could not find node '${ref}'.`);
-            }
-        }
-    }
-}

+ 24 - 30
src/mol-state/tree/immutable.ts

@@ -5,9 +5,8 @@
  */
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 
 export { StateTree }
@@ -17,17 +16,16 @@ export { StateTree }
  * Represented as an immutable map.
  */
 interface StateTree {
-    readonly root: Transform,
+    readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
     readonly cellStates: StateTree.CellStates,
 
-    asTransient(): TransientTree,
-    build(): StateTreeBuilder.Root
+    asTransient(): TransientTree
 }
 
 namespace StateTree {
-    type Ref = Transform.Ref
+    type Ref = StateTransform.Ref
 
     export interface ChildSet {
         readonly size: number,
@@ -43,21 +41,17 @@ namespace StateTree {
         get(ref: Ref): T
     }
 
-    export interface Transforms extends _Map<Transform> {}
+    export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
     export interface CellStates extends _Map<StateObjectCell.State> { }
 
     class Impl implements StateTree {
-        get root() { return this.transforms.get(Transform.RootRef)! }
+        get root() { return this.transforms.get(StateTransform.RootRef)! }
 
         asTransient(): TransientTree {
             return new TransientTree(this);
         }
 
-        build(): StateTreeBuilder.Root {
-            return new StateTreeBuilder.Root(this);
-        }
-
         constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
         }
     }
@@ -65,8 +59,8 @@ namespace StateTree {
     /**
      * Create an instance of an immutable tree.
      */
-    export function createEmpty(): StateTree {
-        const root = Transform.createRoot();
+    export function createEmpty(customRoot?: StateTransform): StateTree {
+        const root = customRoot || StateTransform.createRoot();
         return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]]));
     }
 
@@ -74,10 +68,10 @@ namespace StateTree {
         return new Impl(nodes, children, cellStates);
     }
 
-    type VisitorCtx = { tree: StateTree, state: any, f: (node: Transform, tree: StateTree, state: any) => boolean | undefined | void };
+    type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void };
 
     function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.transforms.get(c!)!); }
-    function _doPostOrder(ctx: VisitorCtx, root: Transform) {
+    function _doPostOrder(ctx: VisitorCtx, root: StateTransform) {
         const children = ctx.tree.children.get(root.ref);
         if (children && children.size) {
             children.forEach(_postOrderFunc, ctx);
@@ -88,14 +82,14 @@ namespace StateTree {
     /**
      * Visit all nodes in a subtree in "post order", meaning leafs get visited first.
      */
-    export function doPostOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S {
+    export function doPostOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S {
         const ctx: VisitorCtx = { tree, state, f };
         _doPostOrder(ctx, root);
         return ctx.state;
     }
 
     function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); }
-    function _doPreOrder(ctx: VisitorCtx, root: Transform) {
+    function _doPreOrder(ctx: VisitorCtx, root: StateTransform) {
         const ret = ctx.f(root, ctx.tree, ctx.state);
         if (typeof ret === 'boolean' && !ret) return;
         const children = ctx.tree.children.get(root.ref);
@@ -108,44 +102,44 @@ namespace StateTree {
      * Visit all nodes in a subtree in "pre order", meaning leafs get visited last.
      * If the visitor function returns false, the visiting for that branch is interrupted.
      */
-    export function doPreOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S {
+    export function doPreOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S {
         const ctx: VisitorCtx = { tree, state, f };
         _doPreOrder(ctx, root);
         return ctx.state;
     }
 
-    function _subtree(n: Transform, _: any, subtree: Transform[]) { subtree.push(n); }
+    function _subtree(n: StateTransform, _: any, subtree: StateTransform[]) { subtree.push(n); }
     /**
      * Get all nodes in a subtree, leafs come first.
      */
-    export function subtreePostOrder<T>(tree: StateTree, root: Transform) {
-        return doPostOrder<Transform[]>(tree, root, [], _subtree);
+    export function subtreePostOrder(tree: StateTree, root: StateTransform) {
+        return doPostOrder<StateTransform[]>(tree, root, [], _subtree);
     }
 
-    function _visitNodeToJson(node: Transform, tree: StateTree, ctx: [Transform.Serialized, StateObjectCell.State][]) {
+    function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) {
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
-        ctx.push([Transform.toJSON(node), tree.cellStates.get(node.ref)]);
+        ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]);
     }
 
     export interface Serialized {
         /** Transforms serialized in pre-order */
-        transforms: [Transform.Serialized, StateObjectCell.State][]
+        transforms: [StateTransform.Serialized, StateObjectCell.State][]
     }
 
-    export function toJSON<T>(tree: StateTree): Serialized {
-        const transforms: [Transform.Serialized, StateObjectCell.State][] = [];
+    export function toJSON(tree: StateTree): Serialized {
+        const transforms: [StateTransform.Serialized, StateObjectCell.State][] = [];
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
     }
 
-    export function fromJSON<T>(data: Serialized): StateTree {
-        const nodes = ImmutableMap<Ref, Transform>().asMutable();
+    export function fromJSON(data: Serialized): StateTree {
+        const nodes = ImmutableMap<Ref, StateTransform>().asMutable();
         const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
         const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
         for (const t of data.transforms) {
-            const transform = Transform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t[0]);
             nodes.set(transform.ref, transform);
             cellStates.set(transform.ref, t[1]);
 

+ 40 - 27
src/mol-state/tree/transient.ts

@@ -5,24 +5,23 @@
  */
 
 import { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 
 export { TransientTree }
 
 class TransientTree implements StateTree {
-    transforms = this.tree.transforms as ImmutableMap<Transform.Ref, Transform>;
-    children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>;
-    cellStates = this.tree.cellStates as ImmutableMap<Transform.Ref, StateObjectCell.State>;
+    transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>;
+    children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>;
+    cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>;
 
     private changedNodes = false;
     private changedChildren = false;
     private changedStates = false;
 
-    private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0;
+    private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0;
 
     private get childMutations() {
         if (this._childMutations) return this._childMutations;
@@ -48,36 +47,32 @@ class TransientTree implements StateTree {
         this.children = this.children.asMutable();
     }
 
-    get root() { return this.transforms.get(Transform.RootRef)! }
-
-    build(): StateTreeBuilder.Root {
-        return new StateTreeBuilder.Root(this);
-    }
+    get root() { return this.transforms.get(StateTransform.RootRef)! }
 
     asTransient() {
         return this.asImmutable().asTransient();
     }
 
-    private addChild(parent: Transform.Ref, child: Transform.Ref) {
+    private addChild(parent: StateTransform.Ref, child: StateTransform.Ref) {
         this.changeChildren();
 
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.add(child);
         } else {
-            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
+            const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable();
             set.add(child);
             this.children.set(parent, set);
             this.childMutations.set(parent, set);
         }
     }
 
-    private removeChild(parent: Transform.Ref, child: Transform.Ref) {
+    private removeChild(parent: StateTransform.Ref, child: StateTransform.Ref) {
         this.changeChildren();
 
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.remove(child);
         } else {
-            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
+            const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable();
             set.remove(child);
             this.children.set(parent, set);
             this.childMutations.set(parent, set);
@@ -85,17 +80,35 @@ class TransientTree implements StateTree {
     }
 
     private clearRoot() {
-        const parent = Transform.RootRef;
+        const parent = StateTransform.RootRef;
         if (this.children.get(parent).size === 0) return;
 
         this.changeChildren();
 
-        const set = OrderedSet<Transform.Ref>();
+        const set = OrderedSet<StateTransform.Ref>();
         this.children.set(parent, set);
         this.childMutations.set(parent, set);
     }
 
-    add(transform: Transform, initialState?: Partial<StateObjectCell.State>) {
+    changeParent(ref: StateTransform.Ref, newParent: StateTransform.Ref) {
+        ensurePresent(this.transforms, ref);
+
+        const old = this.transforms.get(ref);
+        this.removeChild(old.parent, ref);
+        this.addChild(newParent, ref);
+        this.changeNodes();
+        this.transforms.set(ref, StateTransform.withParent(old, newParent));
+    }
+
+    updateVersion(ref: StateTransform.Ref) {
+        ensurePresent(this.transforms, ref);
+
+        const t = this.transforms.get(ref);
+        this.changeNodes();
+        this.transforms.set(ref, StateTransform.withNewVersion(t));
+    }
+
+    add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) {
         const ref = transform.ref;
 
         if (this.transforms.has(transform.ref)) {
@@ -134,7 +147,7 @@ class TransientTree implements StateTree {
     }
 
     /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */
-    setParams(ref: Transform.Ref, params: any) {
+    setParams(ref: StateTransform.Ref, params: any) {
         ensurePresent(this.transforms, ref);
 
         const transform = this.transforms.get(ref)!;
@@ -148,11 +161,11 @@ class TransientTree implements StateTree {
             this.transforms = this.transforms.asMutable();
         }
 
-        this.transforms.set(transform.ref, Transform.withParams(transform, params));
+        this.transforms.set(transform.ref, StateTransform.withParams(transform, params));
         return true;
     }
 
-    updateCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) {
+    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
         ensurePresent(this.transforms, ref);
 
         const old = this.cellStates.get(ref);
@@ -164,12 +177,12 @@ class TransientTree implements StateTree {
         return true;
     }
 
-    remove(ref: Transform.Ref): Transform[] {
+    remove(ref: StateTransform.Ref): StateTransform[] {
         const node = this.transforms.get(ref);
         if (!node) return [];
 
         const st = StateTree.subtreePostOrder(this, node);
-        if (ref === Transform.RootRef) {
+        if (ref === StateTransform.RootRef) {
             st.pop();
             if (st.length === 0) return st;
             this.clearRoot();
@@ -206,17 +219,17 @@ class TransientTree implements StateTree {
     }
 }
 
-function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }
+function fixChildMutations(this: ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>, m: OrderedSet<StateTransform.Ref>, k: StateTransform.Ref) { this.set(k, m.asImmutable()); }
 
-function alreadyPresent(ref: Transform.Ref) {
+function alreadyPresent(ref: StateTransform.Ref) {
     throw new Error(`Transform '${ref}' is already present in the tree.`);
 }
 
-function parentNotPresent(ref: Transform.Ref) {
+function parentNotPresent(ref: StateTransform.Ref) {
     throw new Error(`Parent '${ref}' must be present in the tree.`);
 }
 
-function ensurePresent(nodes: StateTree.Transforms, ref: Transform.Ref) {
+function ensurePresent(nodes: StateTree.Transforms, ref: StateTransform.Ref) {
     if (!nodes.has(ref)) {
         throw new Error(`Node '${ref}' is not present in the tree.`);
     }

+ 2 - 3
src/mol-theme/color.ts

@@ -29,8 +29,6 @@ import { TableLegend } from 'mol-util/color/tables';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
-export type ColorThemeProps = { [k: string]: any }
-
 export { ColorTheme }
 interface ColorTheme<P extends PD.Params> {
     readonly factory: ColorTheme.Factory<P>
@@ -75,4 +73,5 @@ export const BuiltInColorThemes = {
     'shape-group': ShapeGroupColorThemeProvider,
     'unit-index': UnitIndexColorThemeProvider,
     'uniform': UniformColorThemeProvider,
-}
+}
+export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes

+ 2 - 1
src/mol-theme/size.ts

@@ -43,4 +43,5 @@ export const BuiltInSizeThemes = {
     'physical': PhysicalSizeThemeProvider,
     'shape-group': ShapeGroupSizeThemeProvider,
     'uniform': UniformSizeThemeProvider
-}
+}
+export type BuiltInSizeThemeName = keyof typeof BuiltInSizeThemes

+ 2 - 0
src/mol-util/index.ts

@@ -14,6 +14,8 @@ import { Progress } from 'mol-task';
 export * from './value-cell'
 export { BitFlags, StringBuilder, UUID, Mask }
 
+export const noop = function () { };
+
 export function round(n: number, d: number) {
     let f = Math.pow(10, d)
     return Math.round(f * n) / f

+ 53 - 0
src/mol-util/lru-cache.ts

@@ -0,0 +1,53 @@
+
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol.
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { LinkedList } from 'mol-data/generic';
+
+export { LRUCache }
+
+interface LRUCache<T> {
+    entries: LinkedList<LRUCache.Entry<T>>,
+    capacity: number
+}
+
+namespace LRUCache {
+    export interface Entry<T> {
+        key: string,
+        data: T
+    }
+
+    function entry<T>(key: string, data: T): Entry<T> {
+        return { key, data };
+    }
+
+    export function create<T>(capacity: number): LRUCache<T> {
+        return {
+            entries: LinkedList<Entry<T>>(),
+            capacity: Math.max(1, capacity)
+        };
+    }
+
+    export function get<T>(cache: LRUCache<T>, key: string) {
+        for (let e = cache.entries.first; e; e = e.next) {
+            if (e.value.key === key) {
+                cache.entries.remove(e);
+                cache.entries.addLast(e.value);
+                return e.value.data;
+            }
+        }
+        return void 0;
+    }
+
+    export function set<T>(cache: LRUCache<T>, key: string, data: T): T {
+        if (cache.entries.count >= cache.capacity) {
+            cache.entries.remove(cache.entries.first!);
+        }
+        cache.entries.addLast(entry(key, data));
+        return data;
+    }
+}

+ 3 - 2
src/servers/volume/common/binary-schema.ts

@@ -7,6 +7,7 @@
  */
 
 import * as UTF8 from 'mol-io/common/utf8'
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 export type Bool = { kind: 'bool' }
 export type Int = { kind: 'int' }
@@ -105,7 +106,7 @@ export function encode(element: Element, src: any): Buffer {
     return write(element, src);
 }
 
-function decodeElement(e: Element, buffer: Buffer, offset: number, target: { value: any }) {
+function decodeElement(e: Element, buffer: SimpleBuffer, offset: number, target: { value: any }) {
     switch (e.kind) {
         case 'bool': target.value = !!buffer.readInt8(offset); offset += 1; break;
         case 'int': target.value = buffer.readInt32LE(offset); offset += 4; break;
@@ -147,7 +148,7 @@ function decodeElement(e: Element, buffer: Buffer, offset: number, target: { val
     return offset;
 }
 
-export function decode<T>(element: Element, buffer: Buffer, offset?: number) {
+export function decode<T>(element: Element, buffer: SimpleBuffer, offset?: number) {
     const target = { value: void 0 as any };
     decodeElement(element, buffer, offset! | 0, target);
     return target.value as T;

+ 6 - 30
src/servers/volume/common/data-format.ts

@@ -6,18 +6,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as File from './file'
 import * as Schema from './binary-schema'
-
-export type ValueType = 'float32' | 'int8' | 'int16'
-
-export namespace ValueType {
-    export const Float32: ValueType = 'float32';
-    export const Int8: ValueType = 'int8';
-    export const Int16: ValueType = 'int16';
-}
-
-export type ValueArray = Float32Array | Int8Array | Int16Array
+import { FileHandle } from 'mol-io/common/file-handle';
+import { TypedArrayValueType } from 'mol-io/common/typed-array';
 
 export interface Spacegroup {
     number: number,
@@ -62,7 +53,7 @@ export interface Header {
     channels: string[],
 
     /** Determines the data type of the values */
-    valueType: ValueType,
+    valueType: TypedArrayValueType,
 
     /** The value are stored in blockSize^3 cubes */
     blockSize: number,
@@ -102,31 +93,16 @@ namespace _schema {
 
 const headerSchema = _schema.schema;
 
-export function getValueByteSize(type: ValueType) {
-    if (type === ValueType.Float32) return 4;
-    if (type === ValueType.Int16) return 2;
-    return 1;
-}
-
-export function createValueArray(type: ValueType, size: number) {
-    switch (type) {
-        case ValueType.Float32: return new Float32Array(new ArrayBuffer(4 * size));
-        case ValueType.Int8: return new Int8Array(new ArrayBuffer(1 * size));
-        case ValueType.Int16: return new Int16Array(new ArrayBuffer(2 * size));
-    }
-    throw Error(`${type} is not a supported value format.`);
-}
-
 export function encodeHeader(header: Header) {
     return Schema.encode(headerSchema, header);
 }
 
-export async function readHeader(file: number): Promise<{ header: Header, dataOffset: number }> {
-    let { buffer } = await File.readBuffer(file, 0, 4 * 4096);
+export async function readHeader(file: FileHandle): Promise<{ header: Header, dataOffset: number }> {
+    let { buffer } = await file.readBuffer(0, 4 * 4096);
     const headerSize = buffer.readInt32LE(0);
 
     if (headerSize > buffer.byteLength - 4) {
-        buffer = (await File.readBuffer(file, 0, headerSize + 4)).buffer;
+        buffer = (await file.readBuffer(0, headerSize + 4)).buffer;
     }
 
     const header = Schema.decode<Header>(headerSchema, buffer, 4);

+ 5 - 112
src/servers/volume/common/file.ts

@@ -8,9 +8,8 @@
 
 import * as fs from 'fs'
 import * as path from 'path'
-import * as DataFormat from './data-format'
-
-export const IsNativeEndianLittle = new Uint16Array(new Uint8Array([0x12, 0x34]).buffer)[0] === 0x3412;
+import { FileHandle } from 'mol-io/common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 export async function openRead(filename: string) {
     return new Promise<number>((res, rej) => {
@@ -29,43 +28,6 @@ export async function openRead(filename: string) {
     });
 }
 
-export function readBuffer(file: number, position: number, sizeOrBuffer: Buffer | number, size?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: Buffer }> {
-    return new Promise((res, rej) => {
-        if (typeof sizeOrBuffer === 'number') {
-            let buff = new Buffer(new ArrayBuffer(sizeOrBuffer));
-            fs.read(file, buff, 0, sizeOrBuffer, position, (err, bytesRead, buffer) => {
-                if (err) {
-                    rej(err);
-                    return;
-                }
-                res({ bytesRead, buffer });
-            });
-        } else {
-            if (size === void 0) {
-                rej('readBuffer: Specify size.');
-                return;
-            }
-
-            fs.read(file, sizeOrBuffer, byteOffset ? +byteOffset : 0, size, position, (err, bytesRead, buffer) => {
-                if (err) {
-                    rej(err);
-                    return;
-                }
-                res({ bytesRead, buffer });
-            });
-        }
-    })
-}
-
-export function writeBuffer(file: number, position: number, buffer: Buffer, size?: number): Promise<number> {
-    return new Promise<number>((res, rej) => {
-        fs.write(file, buffer, 0, size !== void 0 ? size : buffer.length, position, (err, written) => {
-            if (err) rej(err);
-            else res(written);
-        })
-    })
-}
-
 function makeDir(path: string, root?: string): boolean {
     let dirs = path.split(/\/|\\/g),
         dir = dirs.shift();
@@ -95,77 +57,8 @@ export function createFile(filename: string) {
     });
 }
 
-const __emptyFunc = function () { };
-export function close(file: number | undefined) {
-    try {
-        if (file !== void 0) fs.close(file, __emptyFunc);
-    } catch (e) {
-
-    }
-}
-
-const smallBuffer = new Buffer(8);
-export async function writeInt(file: number, value: number, position: number) {
+const smallBuffer = SimpleBuffer.fromBuffer(new Buffer(8));
+export async function writeInt(file: FileHandle, value: number, position: number) {
     smallBuffer.writeInt32LE(value, 0);
-    await writeBuffer(file, position, smallBuffer, 4);
-}
-
-export interface TypedArrayBufferContext {
-    type: DataFormat.ValueType,
-    elementByteSize: number,
-    readBuffer: Buffer,
-    valuesBuffer: Uint8Array,
-    values: DataFormat.ValueArray
-}
-
-function getElementByteSize(type: DataFormat.ValueType) {
-    if (type === DataFormat.ValueType.Float32) return 4;
-    if (type === DataFormat.ValueType.Int16) return 2;
-    return 1;
-}
-
-function makeTypedArray(type: DataFormat.ValueType, buffer: ArrayBuffer): DataFormat.ValueArray {
-    if (type === DataFormat.ValueType.Float32) return new Float32Array(buffer);
-    if (type === DataFormat.ValueType.Int16) return new Int16Array(buffer);
-    return new Int8Array(buffer);
-}
-
-export function createTypedArrayBufferContext(size: number, type: DataFormat.ValueType): TypedArrayBufferContext {
-    let elementByteSize = getElementByteSize(type);
-    let arrayBuffer = new ArrayBuffer(elementByteSize * size);
-    let readBuffer = new Buffer(arrayBuffer);
-    let valuesBuffer = IsNativeEndianLittle ? arrayBuffer : new ArrayBuffer(elementByteSize * size);
-    return {
-        type,
-        elementByteSize,
-        readBuffer,
-        valuesBuffer: new Uint8Array(valuesBuffer),
-        values: makeTypedArray(type, valuesBuffer)
-    };
-}
-
-function flipByteOrder(source: Buffer, target: Uint8Array, byteCount: number, elementByteSize: number, offset: number) {
-    for (let i = 0, n = byteCount; i < n; i += elementByteSize) {
-        for (let j = 0; j < elementByteSize; j++) {
-            target[offset + i + elementByteSize - j - 1] = source[offset + i + j];
-        }
-    }
-}
-
-export async function readTypedArray(ctx: TypedArrayBufferContext, file: number, position: number, count: number, valueOffset: number, littleEndian?: boolean) {
-    let byteCount = ctx.elementByteSize * count;
-    let byteOffset = ctx.elementByteSize * valueOffset;
-
-    await readBuffer(file, position, ctx.readBuffer, byteCount, byteOffset);
-    if (ctx.elementByteSize > 1 && ((littleEndian !== void 0 && littleEndian !== IsNativeEndianLittle) || !IsNativeEndianLittle)) {
-        // fix the endian 
-        flipByteOrder(ctx.readBuffer, ctx.valuesBuffer, byteCount, ctx.elementByteSize, byteOffset);
-    }
-    return ctx.values;
-}
-
-export function ensureLittleEndian(source: Buffer, target: Buffer, byteCount: number, elementByteSize: number, offset: number) {
-    if (IsNativeEndianLittle) return;
-    if (!byteCount || elementByteSize <= 1) return;
-    flipByteOrder(source, target, byteCount, elementByteSize, offset);
+    await file.writeBuffer(position, smallBuffer, 4);
 }

+ 25 - 5
src/servers/volume/pack.ts

@@ -9,13 +9,30 @@
 import pack from './pack/main'
 import VERSION from './pack/version'
 
-let config = {
-    input: <{ name: string, filename: string }[]>[],
+interface Config {
+    input: { name: string, filename: string }[],
+    format: 'ccp4' | 'dsn6',
+    isPeriodic: boolean,
+    outputFilename: string,
+    blockSizeInMB: number
+}
+
+let config: Config = {
+    input: [],
+    format: 'ccp4',
     isPeriodic: false,
     outputFilename: '',
-    blockSize: 96
+    blockSizeInMB: 96
 };
 
+function getFormat(format: string): Config['format'] {
+    switch (format.toLowerCase()) {
+        case 'ccp4': return 'ccp4'
+        case 'dsn6': return 'dsn6'
+    }
+    throw new Error(`unsupported format '${format}'`)
+}
+
 function printHelp() {
     let help = [
         `VolumeServer Packer ${VERSION}, (c) 2016 - now, David Sehnal`,
@@ -50,7 +67,10 @@ function parseInput() {
     for (let i = 2; i < process.argv.length; i++) {
         switch (process.argv[i].toLowerCase()) {
             case '-blocksize':
-                config.blockSize = +process.argv[++i];
+                config.blockSizeInMB = +process.argv[++i];
+                break;
+            case '-format':
+                config.format = getFormat(process.argv[++i]);
                 break;
             case '-xray':
                 input = true;
@@ -82,5 +102,5 @@ function parseInput() {
 }
 
 if (parseInput()) {
-    pack(config.input, config.blockSize, config.isPeriodic, config.outputFilename);
+    pack(config.input, config.blockSizeInMB, config.isPeriodic, config.outputFilename, config.format);
 }

+ 0 - 163
src/servers/volume/pack/ccp4.ts

@@ -1,163 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import * as File from '../common/file'
-import * as DataFormat from '../common/data-format'
-
-export const enum Mode { Int8 = 0, Int16 = 1, Float32 = 2 }
-
-export interface Header {
-    name: string,
-    mode: Mode,
-    grid: number[], // grid is converted to the axis order!!
-    axisOrder: number[],
-    extent: number[],
-    origin: number[],
-    spacegroupNumber: number,
-    cellSize: number[],
-    cellAngles: number[],
-    littleEndian: boolean,
-    dataOffset: number
-}
-
-/** Represents a circular buffer for 2 * blockSize layers */
-export interface SliceBuffer {
-    buffer: File.TypedArrayBufferContext,
-    sliceCapacity: number,
-    slicesRead: number,
-
-    values: DataFormat.ValueArray,
-    sliceCount: number,
-
-    /** Have all the input slice been read? */
-    isFinished: boolean
-}
-
-export interface Data {
-    header: Header,
-    file: number,
-    slices: SliceBuffer
-}
-
-export function getValueType(header: Header) {
-    if (header.mode === Mode.Float32) return DataFormat.ValueType.Float32;
-    if (header.mode === Mode.Int16) return DataFormat.ValueType.Int16;
-    return DataFormat.ValueType.Int8;
-}
-
-export function assignSliceBuffer(data: Data, blockSize: number) {
-    const { extent } = data.header;
-    const valueType = getValueType(data.header);
-    const sliceSize = extent[0] * extent[1] * DataFormat.getValueByteSize(valueType);
-    const sliceCapacity = Math.max(1, Math.floor(Math.min(64 * 1024 * 1024, sliceSize * extent[2]) / sliceSize));
-    const buffer = File.createTypedArrayBufferContext(sliceCapacity * extent[0] * extent[1], valueType);
-    data.slices = {
-        buffer,
-        sliceCapacity,
-        slicesRead: 0,
-        values: buffer.values,
-        sliceCount: 0,
-        isFinished: false
-    };
-}
-
-function compareProp(a: any, b: any) {
-    if (a instanceof Array && b instanceof Array) {
-        if (a.length !== b.length) return false;
-        for (let i = 0; i < a.length; i++) {
-            if (a[i] !== b[i]) return false;
-        }
-        return true;
-    }
-    return a === b;
-}
-
-export function compareHeaders(a: Header, b: Header) {
-    for (const p of ['grid', 'axisOrder', 'extent', 'origin', 'spacegroupNumber', 'cellSize', 'cellAngles', 'mode']) {
-        if (!compareProp((a as any)[p], (b as any)[p])) return false;
-    }
-    return true;
-}
-
-function getArray(r: (offset: number) => number, offset: number, count: number) {
-    const ret: number[] = [];
-    for (let i = 0; i < count; i++) {
-        ret[i] = r(offset + i);
-    }
-    return ret;
-}
-
-async function readHeader(name: string, file: number) {
-    const headerSize = 1024;
-    const { buffer: data } = await File.readBuffer(file, 0, headerSize);
-
-    let littleEndian = true;
-
-    let mode = data.readInt32LE(3 * 4);
-    if (mode < 0 || mode > 2) {
-        littleEndian = false;
-        mode = data.readInt32BE(3 * 4, true);
-        if (mode < 0 || mode > 2) {
-            throw Error('Only CCP4 modes 0, 1, and 2 are supported.');
-        }
-    }
-
-    const readInt = littleEndian ? (o: number) => data.readInt32LE(o * 4) : (o: number) => data.readInt32BE(o * 4);
-    const readFloat = littleEndian ? (o: number) => data.readFloatLE(o * 4) : (o: number) => data.readFloatBE(o * 4);
-
-    const origin2k = getArray(readFloat, 49, 3);
-    const nxyzStart = getArray(readInt, 4, 3);
-    const header: Header = {
-        name,
-        mode,
-        grid: getArray(readInt, 7, 3),
-        axisOrder: getArray(readInt, 16, 3).map(i => i - 1),
-        extent: getArray(readInt, 0, 3),
-        origin: origin2k[0] === 0.0 && origin2k[1] === 0.0 && origin2k[2] === 0.0 ? nxyzStart : origin2k,
-        spacegroupNumber: readInt(22),
-        cellSize: getArray(readFloat, 10, 3),
-        cellAngles: getArray(readFloat, 13, 3),
-        // mean: readFloat(21),
-        littleEndian,
-        dataOffset: headerSize + readInt(23) /* symBytes */
-    };
-    // "normalize" the grid axis order
-    header.grid = [header.grid[header.axisOrder[0]], header.grid[header.axisOrder[1]], header.grid[header.axisOrder[2]]];
-    return header;
-}
-
-export async function readSlices(data: Data) {
-    const { slices, header } = data;
-    if (slices.isFinished) {
-        return;
-    }
-
-    const { extent } = header;
-    const sliceSize = extent[0] * extent[1];
-    const sliceByteOffset = slices.buffer.elementByteSize * sliceSize * slices.slicesRead;
-    const sliceCount = Math.min(slices.sliceCapacity, extent[2] - slices.slicesRead);
-    const sliceByteCount = sliceCount * sliceSize;
-
-    await File.readTypedArray(slices.buffer, data.file, header.dataOffset + sliceByteOffset, sliceByteCount, 0, header.littleEndian);
-    slices.slicesRead += sliceCount;
-    slices.sliceCount = sliceCount;
-
-    if (slices.slicesRead >= extent[2]) {
-        slices.isFinished = true;
-    }
-}
-
-export async function open(name: string, filename: string): Promise<Data> {
-    const file = await File.openRead(filename);
-    const header = await readHeader(name, file);
-    return {
-        header,
-        file,
-        slices: void 0 as any
-    };
-}

+ 16 - 13
src/servers/volume/pack/data-model.ts

@@ -5,8 +5,11 @@
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
-import * as CCP4 from './ccp4'
+import * as Format from './format'
 import * as DataFormat from '../common/data-format'
+import { FileHandle } from 'mol-io/common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+import { TypedArrayValueArray, TypedArrayValueType } from 'mol-io/common/typed-array';
 
 const FORMAT_VERSION = '1.0.0';
 
@@ -23,16 +26,16 @@ export interface ValuesInfo {
 }
 
 export interface BlockBuffer {
-    values: DataFormat.ValueArray[],
-    buffers: Buffer[],
+    values: TypedArrayValueArray[],
+    buffers: SimpleBuffer[],
     slicesWritten: number
 }
 
 export interface DownsamplingBuffer {
     /** dimensions (sampleCount[1], sampleCount[0] / 2, 1), axis order (K, H, L) */
-    downsampleH: DataFormat.ValueArray,
+    downsampleH: TypedArrayValueArray,
     /** "Cyclic" (in the 1st dimensions) buffer with dimensions (5, sampleCount[0] / 2, sampleCount[1] / 2), axis order (L, H, K),  */
-    downsampleHK: DataFormat.ValueArray,
+    downsampleHK: TypedArrayValueArray,
 
     slicesWritten: number,
     startSliceIndex: number
@@ -68,18 +71,18 @@ export interface Kernel {
 
 export interface Context {
     /** Output file handle  */
-    file: number,
+    file: FileHandle,
 
     /** Periodic are x-ray density files that cover the entire grid and have [0,0,0] origin */
     isPeriodic: boolean,
 
-    channels: CCP4.Data[],
-    valueType: DataFormat.ValueType,
+    channels: Format.Context[],
+    valueType: TypedArrayValueType,
     blockSize: number,
     /** Able to store channels.length * blockSize^3 values. */
-    cubeBuffer: Buffer,
+    cubeBuffer: SimpleBuffer,
     /** All values are stored in little endian format which might not be the native endian of the system  */
-    litteEndianCubeBuffer: Buffer,
+    litteEndianCubeBuffer: SimpleBuffer,
 
     kernel: Kernel,
     sampling: Sampling[],
@@ -90,7 +93,7 @@ export interface Context {
 }
 
 export function createHeader(ctx: Context): DataFormat.Header {
-    const header = ctx.channels[0].header;
+    const header = ctx.channels[0].data.header;
     const grid = header.grid;
 
     function normalize(data: number[]) {
@@ -99,13 +102,13 @@ export function createHeader(ctx: Context): DataFormat.Header {
 
     return {
         formatVersion: FORMAT_VERSION,
-        valueType: CCP4.getValueType(header),
+        valueType: header.valueType,
         blockSize: ctx.blockSize,
         axisOrder: header.axisOrder,
         origin: normalize(header.origin),
         dimensions: normalize(header.extent),
         spacegroup: { number: header.spacegroupNumber, size: header.cellSize, angles: header.cellAngles, isPeriodic: ctx.isPeriodic },
-        channels: ctx.channels.map(c => c.header.name),
+        channels: ctx.channels.map(c => c.data.header.name),
         sampling: ctx.sampling.map(s => {
             const N = s.sampleCount[0] * s.sampleCount[1] * s.sampleCount[2];
             const valuesInfo = [];

+ 11 - 11
src/servers/volume/pack/downsampling.ts

@@ -7,10 +7,10 @@
  */
 
 import * as Data from './data-model'
-import * as DataFormat from '../common/data-format'
+import { TypedArrayValueArray } from 'mol-io/common/typed-array';
 
-/** 
- * Downsamples each slice of input data and checks if there is enough data to perform 
+/**
+ * Downsamples each slice of input data and checks if there is enough data to perform
  * higher rate downsampling.
  */
 export function downsampleLayer(ctx: Data.Context) {
@@ -25,8 +25,8 @@ export function downsampleLayer(ctx: Data.Context) {
     }
 }
 
-/** 
- * When the "native" (rate = 1) sampling is finished, there might still 
+/**
+ * When the "native" (rate = 1) sampling is finished, there might still
  * be some data left to be processed for the higher rate samplings.
  */
 export function finalize(ctx: Data.Context) {
@@ -46,7 +46,7 @@ export function finalize(ctx: Data.Context) {
 /**
  * The functions downsampleH and downsampleHK both essentially do the
  * same thing: downsample along H (1st axis in axis order) and K (2nd axis in axis order) axes respectively.
- * 
+ *
  * The reason there are two copies of almost the same code is performance:
  * Both functions use a different memory layout to improve cache coherency
  *  - downsampleU uses the H axis as the fastest moving one
@@ -54,7 +54,7 @@ export function finalize(ctx: Data.Context) {
  */
 
 
-function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) {
+function conv(w: number, c: number[], src: TypedArrayValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) {
     return w * (c[0] * src[b + i0] + c[1] * src[b + i1] + c[2] * src[b + i2] + c[3] * src[b + i3] + c[4] * src[b + i4]);
 }
 
@@ -63,7 +63,7 @@ function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0:
  * flipping the 1st and 2nd axis in the process to optimize cache coherency for downsampleUV call
  * (i.e. use (K, H, L) axis order).
  */
-function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.ValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) {
+function downsampleH(kernel: Data.Kernel, srcDims: number[], src: TypedArrayValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) {
     const target = buffer.downsampleH;
     const sizeH = srcDims[0], sizeK = srcDims[1], srcBaseOffset = srcLOffset * sizeH * sizeK;
     const targetH = Math.floor((sizeH + 1) / 2);
@@ -87,9 +87,9 @@ function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.Val
     }
 }
 
-/** 
- * Downsample first axis in the slice present in buffer.downsampleH 
- * The result is written into the "cyclical" downsampleHk buffer 
+/**
+ * Downsample first axis in the slice present in buffer.downsampleH
+ * The result is written into the "cyclical" downsampleHk buffer
  * in the (L, H, K) axis order.
  */
 function downsampleHK(kernel: Data.Kernel, dimsX: number[], buffer: Data.DownsamplingBuffer) {

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