浏览代码

Merge branch 'master' into sec-struc

Alexander Rose 6 年之前
父节点
当前提交
19520b0f5c
共有 100 个文件被更改,包括 3554 次插入2149 次删除
  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": [
             "problemMatcher": [
                 "$tsc"
                 "$tsc"
             ]
             ]
-        },
-        {
-            "type": "npm",
-            "script": "app-render-test",
-            "problemMatcher": []
         }
         }
     ]
     ]
 }
 }

+ 6 - 0
docs/state/readme.md

@@ -8,6 +8,8 @@ interface Snapshot {
     data?: State.Snapshot,
     data?: State.Snapshot,
     // Snapshot of behavior state tree
     // Snapshot of behavior state tree
     behaviour?: State.Snapshot,
     behaviour?: State.Snapshot,
+    // Snapshot for current animation,
+    animation?: PluginAnimationManager.Snapshot,
     // Saved camera positions
     // Saved camera positions
     cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
     cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
     canvas3d?: {
     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).
 "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
 # Canvas3D State
 
 
 Defined by ``Canvas3DParams`` in ``mol-canvas3d/canvas3d.ts``.
 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-ccp4](#ms-plugin-parse-ccp4)
 * [ms-plugin.parse-dsn6](#ms-plugin-parse-dsn6)
 * [ms-plugin.parse-dsn6](#ms-plugin-parse-dsn6)
 * [ms-plugin.trajectory-from-mmcif](#ms-plugin-trajectory-from-mmcif)
 * [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.model-from-trajectory](#ms-plugin-model-from-trajectory)
 * [ms-plugin.structure-from-model](#ms-plugin-structure-from-model)
 * [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-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-selection](#ms-plugin-structure-selection)
 * [ms-plugin.structure-complex-element](#ms-plugin-structure-complex-element)
 * [ms-plugin.structure-complex-element](#ms-plugin-structure-complex-element)
 * [ms-plugin.custom-model-properties](#ms-plugin-custom-model-properties)
 * [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
 ## <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
 ## <a name="ms-plugin-parse-dsn6"></a>ms-plugin.parse-dsn6 :: Binary -> Dsn6
@@ -82,6 +84,9 @@
 ```js
 ```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
 ## <a name="ms-plugin-model-from-trajectory"></a>ms-plugin.model-from-trajectory :: Trajectory -> Model
 *Create a molecular structure from the specified model.*
 *Create a molecular structure from the specified model.*
@@ -104,13 +109,36 @@
 *Create a molecular structure assembly.*
 *Create a molecular structure assembly.*
 
 
 ### Parameters
 ### 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
 ### Default Parameters
 ```js
 ```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
 ## <a name="ms-plugin-structure-selection"></a>ms-plugin.structure-selection :: Structure -> Structure
 *Create a molecular structure from the specified query expression.*
 *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
 ## <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
 ### Parameters
 - **voxelSize**: 3D vector [x, y, z]
 - **voxelSize**: 3D vector [x, y, z]
@@ -294,7 +322,7 @@ Object with:
       - **highlightColor**: Color as 0xrrggbb
       - **highlightColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
       - **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'
       - **renderMode**: One of 'isosurface', 'volume'
       - **controlPoints**: A list of 2d vectors [xi, yi][]
       - **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'
       - **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
       - **highlightColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **selectColor**: Color as 0xrrggbb
       - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
       - **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'
       - **renderMode**: One of 'isosurface', 'volume'
       - **controlPoints**: A list of 2d vectors [xi, yi][]
       - **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'
       - **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'

文件差异内容过多而无法显示
+ 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": "concurrently --kill-others \"npm:watch-ts\" \"npm:watch-extra\" \"npm:watch-webpack\"",
     "watch-ts": "tsc -watch",
     "watch-ts": "tsc -watch",
     "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --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",
     "watch-webpack": "webpack -w --mode development",
     "model-server": "node build/src/servers/model/server.js",
     "model-server": "node build/src/servers/model/server.js",
     "model-server-watch": "nodemon --watch build/src 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/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.1",
     "@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",
     "@types/webgl2": "0.0.4",
     "benchmark": "^2.1.4",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
     "circular-dependency-plugin": "^5.0.2",
@@ -96,22 +96,22 @@
     "glslify-import": "^3.1.0",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^2.0.0",
     "glslify-loader": "^2.0.0",
     "graphql-code-generator": "^0.16.1",
     "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",
     "graphql-codegen-typescript-template": "^0.16.1",
-    "jest": "^23.6.0",
+    "jest": "^24.1.0",
     "jest-raw-loader": "^1.0.1",
     "jest-raw-loader": "^1.0.1",
     "mini-css-extract-plugin": "^0.5.0",
     "mini-css-extract-plugin": "^0.5.0",
     "node-sass": "^4.11.0",
     "node-sass": "^4.11.0",
     "raw-loader": "^1.0.0",
     "raw-loader": "^1.0.0",
-    "resolve-url-loader": "^3.0.0",
+    "resolve-url-loader": "^3.0.1",
     "sass-loader": "^7.1.0",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
     "style-loader": "^0.23.1",
-    "ts-jest": "^23.10.5",
+    "ts-jest": "^24.0.0",
     "tslint": "^5.12.1",
     "tslint": "^5.12.1",
-    "typescript": "^3.3.1",
+    "typescript": "^3.3.3",
     "uglify-js": "^3.4.9",
     "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.29.3",
+    "webpack": "^4.29.5",
     "webpack-cli": "^3.2.3"
     "webpack-cli": "^3.2.3"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -121,8 +121,8 @@
     "graphql": "^14.1.1",
     "graphql": "^14.1.1",
     "immutable": "^3.8.2",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
     "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 {
             #app {
                 position: absolute;
                 position: absolute;
-                left: 100px;
+                left: 160px;
                 top: 100px;
                 top: 100px;
                 width: 600px;
                 width: 600px;
                 height: 600px;
                 height: 600px;
                 border: 1px solid #ccc;
                 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>
         </style>
         <link rel="stylesheet" type="text/css" href="app.css" />
         <link rel="stylesheet" type="text/css" href="app.css" />
         <script type="text/javascript" src="./index.js"></script>
         <script type="text/javascript" src="./index.js"></script>
     </head>
     </head>
     <body>
     <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>
         <div id="app"></div>
         <script>            
         <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 url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
             var format = 'cif';
             var format = 'cif';
 
 
             // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
             // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
             // var format = 'pdb';
             // var format = 'pdb';
+            // var assemblyId = 'deposited';
 
 
             BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
             BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
             BasicMolStarWrapper.setBackground(0xffffff);
             BasicMolStarWrapper.setBackground(0xffffff);
             BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
             BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
             BasicMolStarWrapper.toggleSpin();
             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>
         </script>
     </body>
     </body>
 </html>
 </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 { StateTransforms } from 'mol-plugin/state/transforms';
 import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
 import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
 import { Color } from 'mol-util/color';
 import { Color } from 'mol-util/color';
-import { StateTreeBuilder } from 'mol-state/tree/builder';
 import { PluginStateObject as PSO } from 'mol-plugin/state/objects';
 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')
 require('mol-plugin/skin/light.scss')
 
 
 type SupportedFormats = 'cif' | 'pdb'
 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 })
         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'
         const parsed = format === 'cif'
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
             : b.apply(StateTransforms.Model.TrajectoryFromPDB);
@@ -45,7 +46,7 @@ class BasicWrapper {
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
             .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' })
         visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
             .apply(StateTransforms.Representation.StructureRepresentation3D,
             .apply(StateTransforms.Representation.StructureRepresentation3D,
                 StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon'));
                 StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon'));
@@ -73,7 +74,7 @@ class BasicWrapper {
             if (state.select('asm').length > 0) loadType = 'update';
             if (state.select('asm').length > 0) loadType = 'update';
         }
         }
 
 
-        let tree: StateTreeBuilder.Root;
+        let tree: StateBuilder.Root;
         if (loadType === 'full') {
         if (loadType === 'full') {
             await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
             await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
             tree = state.build();
             tree = state.build();
@@ -98,6 +99,17 @@ class BasicWrapper {
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
         PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
         if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { });
         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();
 (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 * as _ from 'mol-plugin/state/transforms'
-import { Transformer, StateObject } from 'mol-state';
+import { StateTransformer, StateObject } from 'mol-state';
 import { StringBuilder } from 'mol-util';
 import { StringBuilder } from 'mol-util';
 import * as fs from 'fs';
 import * as fs from 'fs';
 import { paramsToMd } from './pd-to-md';
 import { paramsToMd } from './pd-to-md';
@@ -28,7 +28,7 @@ function typeToString(o: StateObject.Ctor[]) {
     return o.map(o => o.name).join(' | ');
     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.write(builder, `## <a name="${t.id.replace('.', '-')}"></a>${t.id} :: ${typeToString(t.definition.from)} -> ${typeToString(t.definition.to)}`);
     StringBuilder.newline(builder);
     StringBuilder.newline(builder);
     if (t.definition.display.description) {
     if (t.definition.display.description) {
@@ -52,7 +52,7 @@ function writeTransformer(t: Transformer) {
     StringBuilder.newline(builder);
     StringBuilder.newline(builder);
 }
 }
 
 
-const transformers = Transformer.getAll();
+const transformers = StateTransformer.getAll();
 
 
 StringBuilder.write(builder, '# Mol* Plugin State Transformer Reference');
 StringBuilder.write(builder, '# Mol* Plugin State Transformer Reference');
 StringBuilder.newline(builder);
 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) {
 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 });
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
 
     // Export the mesh in OBJ format.
     // Export the mesh in OBJ format.

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

@@ -192,7 +192,7 @@ namespace Canvas3D {
             if (isIdentifying || isUpdating) return false
             if (isIdentifying || isUpdating) return false
 
 
             let didRender = 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.
             // 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();
             if (!camera.transition.inTransition) setClipping();
             const cameraChanged = camera.updateMatrices();
             const cameraChanged = camera.updateMatrices();
@@ -230,6 +230,7 @@ namespace Canvas3D {
         }
         }
 
 
         let forceNextDraw = false;
         let forceNextDraw = false;
+        let currentTime = 0;
 
 
         function draw(force?: boolean) {
         function draw(force?: boolean) {
             if (render('draw', !!force || forceNextDraw)) {
             if (render('draw', !!force || forceNextDraw)) {
@@ -246,8 +247,8 @@ namespace Canvas3D {
         }
         }
 
 
         function animate() {
         function animate() {
-            const t = now();
-            camera.transition.tick(t);
+            currentTime = now();
+            camera.transition.tick(currentTime);
             draw(false)
             draw(false)
             window.requestAnimationFrame(animate)
             window.requestAnimationFrame(animate)
         }
         }

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

@@ -39,12 +39,12 @@ interface TrackballControls {
     readonly props: Readonly<TrackballControlsProps>
     readonly props: Readonly<TrackballControlsProps>
     setProps: (props: Partial<TrackballControlsProps>) => void
     setProps: (props: Partial<TrackballControlsProps>) => void
 
 
-    update: () => void
+    update: (t: number) => void
     reset: () => void
     reset: () => void
     dispose: () => void
     dispose: () => void
 }
 }
 namespace TrackballControls {
 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 p = { ...PD.getDefaultValues(TrackballControlsParams), ...props }
 
 
         const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 }
         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))
                 Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
 
 
                 angle *= p.rotateSpeed;
                 angle *= p.rotateSpeed;
-                Quat.setAxisAngle(rotQuat, rotAxis, angle )
+                Quat.setAxisAngle(rotQuat, rotAxis, angle)
 
 
                 Vec3.transformQuat(_eye, _eye, rotQuat)
                 Vec3.transformQuat(_eye, _eye, rotQuat)
                 Vec3.transformQuat(object.up, object.up, rotQuat)
                 Vec3.transformQuat(object.up, object.up, rotQuat)
@@ -150,7 +150,7 @@ namespace TrackballControls {
             Vec2.copy(_movePrev, _moveCurr)
             Vec2.copy(_movePrev, _moveCurr)
         }
         }
 
 
-        function zoomCamera () {
+        function zoomCamera() {
             const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed
             const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed
             if (factor !== 1.0 && factor > 0.0) {
             if (factor !== 1.0 && factor > 0.0) {
                 Vec3.scale(_eye, _eye, factor)
                 Vec3.scale(_eye, _eye, factor)
@@ -207,8 +207,12 @@ namespace TrackballControls {
             }
             }
         }
         }
 
 
+        let lastUpdated = -1;
         /** Update the object's position, direction and up vectors */
         /** 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)
             Vec3.sub(_eye, object.position, target)
 
 
             rotateCamera()
             rotateCamera()
@@ -224,6 +228,8 @@ namespace TrackballControls {
             if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) {
             if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) {
                 Vec3.copy(lastPosition, object.position)
                 Vec3.copy(lastPosition, object.position)
             }
             }
+
+            lastUpdated = t;
         }
         }
 
 
         /** Reset object's vectors and the target vector to their initial values */
         /** 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);
         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 (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed);
-            if (p.spin) requestAnimationFrame(spin);
         }
         }
 
 
         // force an update at start
         // force an update at start
-        update();
-
-        if (props.spin) { spin(); }
+        update(0);
 
 
         return {
         return {
             viewport,
             viewport,
 
 
             get props() { return p as Readonly<TrackballControlsProps> },
             get props() { return p as Readonly<TrackballControlsProps> },
             setProps: (props: Partial<TrackballControlsProps>) => {
             setProps: (props: Partial<TrackballControlsProps>) => {
-                const wasSpinning = p.spin
                 Object.assign(p, props)
                 Object.assign(p, props)
-                if (p.spin && !wasSpinning) requestAnimationFrame(spin)
             },
             },
 
 
             update,
             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 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 {
 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 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.
      * @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 namespace FileHandle {
-    export function fromBuffer(buffer: Uint8Array): FileHandle {
+    export function fromBuffer(buffer: SimpleBuffer): FileHandle {
         return {
         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') {
                 if (typeof sizeOrBuffer === 'number') {
+                    size = defaults(size, sizeOrBuffer)
                     const start = position
                     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 {
                 } else {
-                    if (size === void 0) {
-                        return Promise.reject('readBuffer: Specify size.');
-                    }
+                    size = defaults(size, sizeOrBuffer.length)
                     const start = position
                     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)
                     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>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -8,122 +8,164 @@ import { Task, RuntimeContext } from 'mol-task';
 import { Ccp4File, Ccp4Header } from './schema'
 import { Ccp4File, Ccp4Header } from './schema'
 import { ReaderResult as Result } from '../result'
 import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
 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
     // 53  MAP         Character string 'MAP ' to identify file type
     const MAP = String.fromCharCode(
     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 ') {
     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
     // 54  MACHST      Machine stamp indicating machine type which wrote file
     //                 17 and 17 for big-endian or 68 and 65 for little-endian
     //                 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
     // found MRC files that don't have the MACHST stamp set and are big-endian
     if (MACHST[0] !== 68 && MACHST[1] !== 65) {
     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 = {
     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
         SKWMAT: [], // TODO bytes 26-34
         SKWTRN: [], // TODO bytes 35-37
         SKWTRN: [], // TODO bytes 35-37
 
 
+        userFlag1: readInt(39),
+        userFlag2: readInt(40),
+
         // bytes 50-52 origin in X,Y,Z used for transforms
         // 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
         MAP, // bytes 53 MAP
         MACHST, // bytes 54 MACHST
         MACHST, // bytes 54 MACHST
 
 
-        ARMS: floatView[54],
+        ARMS: readFloat(54),
 
 
         // TODO bytes 56 NLABL
         // TODO bytes 56 NLABL
         // TODO bytes 57-256 LABEL
         // 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
         // scaling f(x)=b1*x+b0 such that f(-128)=min and f(127)=max
         const b1 = (header.AMAX - header.AMIN) / 255.0
         const b1 = (header.AMAX - header.AMIN) / 255.0
         const b0 = 0.5 * (header.AMIN + header.AMAX + b1)
         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) {
 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
      * May be used in CCP4 but not in MRC
      */
      */
     SKWTRN: number[]
     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) */
     /** x axis origin transformation (not used in CCP4) */
     originX: number
     originX: number
     /** y axis origin transformation (not used in CCP4) */
     /** y axis origin transformation (not used in CCP4) */
@@ -112,5 +115,5 @@ export interface Ccp4Header {
  */
  */
 export interface Ccp4File {
 export interface Ccp4File {
     header: Ccp4Header
     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 { Dsn6File, Dsn6Header } from './schema'
 import { ReaderResult as Result } from '../result'
 import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
 import { FileHandle } from '../../common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+
+export const dsn6HeaderSize = 512;
 
 
 function parseBrixHeader(str: string): Dsn6Header {
 function parseBrixHeader(str: string): Dsn6Header {
     return {
     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 {
     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
         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 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
     // loop over blocks
     for (let zz = 0; zz < zBlocks; ++zz) {
     for (let zz = 0; zz < zBlocks; ++zz) {
         for (let yy = 0; yy < yBlocks; ++yy) {
         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) {
                         for (let i = 0; i < 8; ++i) {
                             const x = 8 * xx + i
                             const x = 8 * xx + i
                             // check if remaining slice-part contains values
                             // 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
                                 ++offset
                             } else {
                             } else {
                                 offset += 8 - i
                                 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 };
     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) {
 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 {
 export interface Dsn6File {
     header: Dsn6Header
     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 { Task } from 'mol-task';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { SpacegroupCell, Box3D } from 'mol-math/geometry';
 import { Tensor, Vec3 } from 'mol-math/linear-algebra';
 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 { 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 => {
     return Task.create<VolumeData>('Create Volume Data', async ctx => {
         const { header, values } = source;
         const { header, values } = source;
+        console.log({ header, values })
         const size = Vec3.create(header.xLength, header.yLength, header.zLength)
         const size = Vec3.create(header.xLength, header.yLength, header.zLength)
         if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize)
         if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize)
         const angles = Vec3.create(degToRad(header.alpha), degToRad(header.beta), degToRad(header.gamma))
         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 grid = [header.NX, header.NY, header.NZ];
         const extent = normalizeOrder([header.NC, header.NR, header.NS]);
         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 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 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));
         const data = Tensor.create(space, Tensor.Data1(values));
 
 
         // TODO Calculate stats? When to trust header data?
         // 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 {
 namespace VolumeData {
-    export const Empty: VolumeData = {
+    export const One: VolumeData = {
         cell: SpacegroupCell.Zero,
         cell: SpacegroupCell.Zero,
         fractionalBox: Box3D.empty(),
         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 }
         dataStats: { min: 0, max: 0, mean: 0, sigma: 0 }
     }
     }
 
 
@@ -44,11 +44,11 @@ namespace VolumeData {
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative
 type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative
 
 
 namespace VolumeIsoValue {
 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 {
     export function calcAbsolute(stats: VolumeData['dataStats'], relativeValue: number): number {
         return relativeValue * stats.sigma + stats.mean
         return relativeValue * stats.sigma + stats.mean
@@ -58,22 +58,18 @@ namespace VolumeIsoValue {
         return stats.sigma === 0 ? 0 : ((absoluteValue - stats.mean) / stats.sigma)
         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 { PluginStateTransform, PluginStateObject } from '../state/objects';
-import { Transformer, Transform } from 'mol-state';
+import { StateTransformer, StateTransform } from 'mol-state';
 import { Task } from 'mol-task';
 import { Task } from 'mol-task';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommand } from '../command';
 import { PluginCommand } from '../command';
@@ -16,7 +16,7 @@ import { shallowEqual } from 'mol-util';
 export { PluginBehavior }
 export { PluginBehavior }
 
 
 interface PluginBehavior<P = unknown> {
 interface PluginBehavior<P = unknown> {
-    register(ref: Transform.Ref): void,
+    register(ref: StateTransform.Ref): void,
     unregister(): void,
     unregister(): void,
 
 
     /** Update params in place. Optionally return a promise if it depends on an async action. */
     /** 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 {
 namespace PluginBehavior {
     export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { }
     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 class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { }
 
 
     export interface Ctor<P = undefined> { new(ctx: PluginContext, params: P): PluginBehavior<P> }
     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> {
     export interface CreateParams<P> {
         name: string,
         name: string,
+        category: keyof typeof Categories,
         ctor: Ctor<P>,
         ctor: Ctor<P>,
-        canAutoUpdate?: Transformer.Definition<Root, Behavior, P>['canAutoUpdate'],
+        canAutoUpdate?: StateTransformer.Definition<Root, Behavior, P>['canAutoUpdate'],
         label?: (params: P) => { label: string, description?: string },
         label?: (params: P) => { label: string, description?: string },
         display: {
         display: {
             name: string,
             name: string,
@@ -42,9 +52,28 @@ namespace PluginBehavior {
         params?(a: Root, globalCtx: PluginContext): { [K in keyof P]: ParamDefinition.Any }
         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>) {
     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,
             name: params.name,
             display: params.display,
             display: params.display,
             from: [Root],
             from: [Root],
@@ -56,13 +85,15 @@ namespace PluginBehavior {
             },
             },
             update({ b, newParams }) {
             update({ b, newParams }) {
                 return Task.create('Update Behavior', async () => {
                 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);
                     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
             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>) {
     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 { degToRad } from 'mol-math/misc';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
 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 { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
 import { UUID } from 'mol-util';
 import { UUID } from 'mol-util';
 
 
@@ -30,6 +29,7 @@ type StructureAnimationProps = PD.Values<typeof StructureAnimationParams>
  */
  */
 export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({
 export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({
     name: 'structure-animation',
     name: 'structure-animation',
+    category: 'representation',
     display: { name: 'Structure Animation', group: 'Animation' },
     display: { name: 'Structure Animation', group: 'Animation' },
     canAutoUpdate: () => true,
     canAutoUpdate: () => true,
     ctor: class extends PluginBehavior.Handler<StructureAnimationProps> {
     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 }>({
 export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({
     name: 'focus-loci-on-select',
     name: 'focus-loci-on-select',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> {
     ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> {
         register(): void {
         register(): void {
             this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {
             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 }>({
 export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'pdbe-structure-quality-report-prop',
     name: 'pdbe-structure-quality-report-prop',
+    category: 'custom-props',
     display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
     display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = StructureQualityReport.createAttachTask(
         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 }>({
 export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
     name: 'rcsb-assembly-symmetry-prop',
     name: 'rcsb-assembly-symmetry-prop',
+    category: 'custom-props',
     display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
     display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
         private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch);
         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 { ParamDefinition as PD } from 'mol-util/param-definition'
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { Mat4, Vec3 } from 'mol-math/linear-algebra';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
 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 { RuntimeContext } from 'mol-task';
 import { Shape } from 'mol-model/shape';
 import { Shape } from 'mol-model/shape';
 import { Text } from 'mol-geo/geometry/text/text';
 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>({
 export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
     name: 'scene-labels',
     name: 'scene-labels',
+    category: 'representation',
     display: { name: 'Scene Labels', group: 'Labels' },
     display: { name: 'Scene Labels', group: 'Labels' },
     canAutoUpdate: () => true,
     canAutoUpdate: () => true,
     ctor: class extends PluginBehavior.Handler<SceneLabelsProps> {
     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 { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginStateObject } from 'mol-plugin/state/objects';
 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 { labelFirst } from 'mol-theme/label';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { PluginBehavior } from '../behavior';
 import { PluginBehavior } from '../behavior';
 
 
 export const HighlightLoci = PluginBehavior.create({
 export const HighlightLoci = PluginBehavior.create({
     name: 'representation-highlight-loci',
     name: 'representation-highlight-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
     ctor: class extends PluginBehavior.Handler {
         register(): void {
         register(): void {
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
@@ -38,6 +38,7 @@ export const HighlightLoci = PluginBehavior.create({
 
 
 export const SelectLoci = PluginBehavior.create({
 export const SelectLoci = PluginBehavior.create({
     name: 'representation-select-loci',
     name: 'representation-select-loci',
+    category: 'interaction',
     ctor: class extends PluginBehavior.Handler {
     ctor: class extends PluginBehavior.Handler {
         register(): void {
         register(): void {
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
             let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0;
@@ -60,6 +61,7 @@ export const SelectLoci = PluginBehavior.create({
 
 
 export const DefaultLociLabelProvider = PluginBehavior.create({
 export const DefaultLociLabelProvider = PluginBehavior.create({
     name: 'default-loci-label-provider',
     name: 'default-loci-label-provider',
+    category: 'interaction',
     ctor: class implements PluginBehavior<undefined> {
     ctor: class implements PluginBehavior<undefined> {
         private f = labelFirst;
         private f = labelFirst;
         register(): void { this.ctx.lociLabels.addProvider(this.f); }
         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 { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
 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 { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
 import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
 import { EmptyLoci, EveryLoci } from 'mol-model/loci';
 import { EmptyLoci, EveryLoci } from 'mol-model/loci';
@@ -51,7 +51,7 @@ export function SetCurrentObject(ctx: PluginContext) {
 }
 }
 
 
 export function Update(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) {
 export function ApplyAction(ctx: PluginContext) {
@@ -59,9 +59,27 @@ export function ApplyAction(ctx: PluginContext) {
 }
 }
 
 
 export function RemoveObject(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));
         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));
     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);
     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 });
     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 { Camera } from 'mol-canvas3d/camera';
 import { PluginCommand } from './command/base';
 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 { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginLayoutStateProps } from './layout';
 import { PluginLayoutStateProps } from './layout';
 
 
@@ -15,16 +14,16 @@ export * from './command/base';
 
 
 export const PluginCommands = {
 export const PluginCommands = {
     State: {
     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: {
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),
             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>
  * @author David Sehnal <david.sehnal@gmail.com>
  */
  */
 
 
-import { BehaviorSubject, Observable, Subject } from 'rxjs';
-import { PluginContext } from './context';
 import { shallowMergeArray } from 'mol-util/object';
 import { shallowMergeArray } from 'mol-util/object';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
 
 
 export class PluginComponent<State> {
 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 { EmptyLoci, Loci } from 'mol-model/loci';
 import { Representation } from 'mol-repr/representation';
 import { Representation } from 'mol-repr/representation';
 import { StructureRepresentationRegistry } from 'mol-repr/structure/registry';
 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 { Task } from 'mol-task';
 import { ColorTheme } from 'mol-theme/color';
 import { ColorTheme } from 'mol-theme/color';
 import { SizeTheme } from 'mol-theme/size';
 import { SizeTheme } from 'mol-theme/size';
@@ -30,7 +30,8 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { PluginLayout } from './layout';
 import { PluginLayout } from './layout';
 import { List } from 'immutable';
 import { List } from 'immutable';
 import { StateTransformParameters } from './ui/state/common';
 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 {
 export class PluginContext {
     private disposed = false;
     private disposed = false;
@@ -98,7 +99,7 @@ export class PluginContext {
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
         try {
             this.layout.setRoot(container);
             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);
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } });
             this.canvas3d.animate();
             this.canvas3d.animate();
@@ -140,15 +141,16 @@ export class PluginContext {
         this.ev.dispose();
         this.ev.dispose();
         this.state.dispose();
         this.state.dispose();
         this.tasks.dispose();
         this.tasks.dispose();
+        this.layout.dispose();
         this.disposed = true;
         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 });
         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);
         const tree = state.build().to(a).update(params);
         return PluginCommands.State.Update.dispatch(this, { state, tree });
         return PluginCommands.State.Update.dispatch(this, { state, tree });
     }
     }
@@ -163,10 +165,14 @@ export class PluginContext {
     }
     }
 
 
     private async initBehaviors() {
     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) {
         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));
         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() {
     private initCustomParamEditors() {
         if (!this.spec.customParamEditors) return;
         if (!this.spec.customParamEditors) return;
 
 
@@ -193,6 +206,7 @@ export class PluginContext {
 
 
         this.initBehaviors();
         this.initBehaviors();
         this.initDataActions();
         this.initDataActions();
+        this.initAnimations();
         this.initCustomParamEditors();
         this.initCustomParamEditors();
 
 
         this.lociLabels = new LociLabelManager(this);
         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 * as ReactDOM from 'react-dom';
 import { PluginCommands } from './command';
 import { PluginCommands } from './command';
 import { PluginSpec } from './spec';
 import { PluginSpec } from './spec';
-import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
 import { PluginBehaviors } from './behavior';
+import { AnimateModelIndex } from './state/animation/built-in';
+import { StateActions } from './state/actions';
 
 
 function getParam(name: string, regex: string): string {
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -22,11 +23,15 @@ function getParam(name: string, regex: string): string {
 
 
 export const DefaultPluginSpec: PluginSpec = {
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
     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.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Data.ParseCcp4),
         PluginSpec.Action(StateTransforms.Data.ParseCcp4),
@@ -34,7 +39,7 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.StructureFromModel),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
         PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory),
-        PluginSpec.Action(StateTransforms.Model.VolumeFromCcp4),
+        PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4),
         PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
         PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D),
@@ -48,6 +53,9 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels),
         PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { 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> {
 export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
+    readonly events = {
+        updated: this.ev()
+    }
+
     private updateProps(state: Partial<PluginLayoutStateProps>) {
     private updateProps(state: Partial<PluginLayoutStateProps>) {
-        let prevExpanded = !!this.latestState.isExpanded;
+        let prevExpanded = !!this.state.isExpanded;
         this.updateState(state);
         this.updateState(state);
         if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
         if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
 
 
-        this.triggerUpdate();
+        this.events.updated.next();
     }
     }
 
 
     private root: HTMLElement;
     private root: HTMLElement;
     private rootState: RootState | undefined = void 0;
     private rootState: RootState | undefined = void 0;
     private expandedViewport: HTMLMetaElement;
     private expandedViewport: HTMLMetaElement;
 
 
+    setProps(props: PluginLayoutStateProps) {
+        this.updateState(props);
+    }
+
     setRoot(root: HTMLElement) {
     setRoot(root: HTMLElement) {
         this.root = root;
         this.root = root;
-        if (this.latestState.isExpanded) this.handleExpand();
+        if (this.state.isExpanded) this.handleExpand();
     }
     }
 
 
     private getScrollElement() {
     private getScrollElement() {
@@ -72,7 +80,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
 
 
             if (!body || !head) return;
             if (!body || !head) return;
 
 
-            if (this.latestState.isExpanded) {
+            if (this.state.isExpanded) {
                 let children = head.children;
                 let children = head.children;
                 let hasExp = false;
                 let hasExp = false;
                 let viewports: HTMLElement[] = [];
                 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));
         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 {
     > div:first-child {
         position: absolute;
         position: absolute;
         top: 0;
         top: 0;
-        left: 0;
+        left: 18px;
         bottom: 0;
         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 {
     > div:last-child {
         position: absolute;
         position: absolute;
@@ -101,9 +92,12 @@
         bottom: 0;
         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] {
     // input[type=range] {
     //     width: 100%;
     //     width: 100%;
@@ -125,20 +119,10 @@
     > div:nth-child(2) {
     > div:nth-child(2) {
         position: absolute;
         position: absolute;
         top: 0;
         top: 0;
-        left: 0;
+        left: 35px;
         bottom: 0;
         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 {
     > div:last-child {
         position: absolute;
         position: absolute;
@@ -152,9 +136,12 @@
         font-size: 80%;
         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] {
     // input[type=range] {
     //     width: 100%;
     //     width: 100%;

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

@@ -66,4 +66,8 @@
     background: white;
     background: white;
     cursor: inherit;
     cursor: inherit;
     display: block;
     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;
   padding: 5px 0;
   width: 100%;
   width: 100%;
   border-radius: $slider-border-radius-base;
   border-radius: $slider-border-radius-base;
+  align-self: center;
   @include borderBox;
   @include borderBox;
 
 
   &-rail {
   &-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
     // 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 {
 .msp-btn-row-group {
     display:flex;
     display:flex;
     flex-direction:row;
     flex-direction:row;
@@ -69,10 +78,32 @@
     margin-bottom: 1px;
     margin-bottom: 1px;
     padding-left: $row-height;
     padding-left: $row-height;
     padding-right: 2 * $row-height + $control-spacing;
     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 {
     &-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;
     left: 0;
     top: 0;
     top: 0;
     width: $row-height;
     width: $row-height;
+    padding: 0;
     color: color-lower-contrast($font-color, 24%);
     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;
     background: $control-background;
 }
 }
 
 
@@ -42,14 +42,44 @@
     margin-bottom: $control-spacing;
     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 {
 .msp-transform-header {
     position: relative;
     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 {
     > button:hover {
@@ -58,9 +88,11 @@
 }
 }
 
 
 .msp-transform-default-params {
 .msp-transform-default-params {
+    background: $default-background;
     position: absolute;
     position: absolute;
-    right: 0;
+    left: 0;
     top: 0;
     top: 0;
+    width: $row-height;
 }
 }
 
 
 .msp-transform-default-params:hover {
 .msp-transform-default-params:hover {
@@ -74,7 +106,8 @@
 }
 }
 
 
 .msp-transform-refresh {
 .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;
     background: $default-background;
     text-align: right;
     text-align: right;
 }
 }

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

@@ -4,37 +4,38 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  * @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 { StateTransformParameters } from './ui/state/common';
 import { PluginLayoutStateProps } from './layout';
 import { PluginLayoutStateProps } from './layout';
+import { PluginStateAnimation } from './state/animation/model';
 
 
 export { PluginSpec }
 export { PluginSpec }
 
 
 interface PluginSpec {
 interface PluginSpec {
     actions: PluginSpec.Action[],
     actions: PluginSpec.Action[],
     behaviors: PluginSpec.Behavior[],
     behaviors: PluginSpec.Behavior[],
-    customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][]
+    animations?: PluginStateAnimation[],
+    customParamEditors?: [StateAction | StateTransformer, StateTransformParameters.Class][],
     initialLayout?: PluginLayoutStateProps
     initialLayout?: PluginLayoutStateProps
 }
 }
 
 
 namespace PluginSpec {
 namespace PluginSpec {
     export interface Action {
     export interface Action {
-        action: StateAction | Transformer,
+        action: StateAction | StateTransformer,
         customControl?: StateTransformParameters.Class,
         customControl?: StateTransformParameters.Class,
         autoUpdate?: boolean
         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 };
         return { action, customControl: params && params.customControl, autoUpdate: params && params.autoUpdate };
     }
     }
 
 
     export interface Behavior {
     export interface Behavior {
-        transformer: Transformer,
+        transformer: StateTransformer,
         defaultParams?: any
         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 };
         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 { RxEventHelper } from 'mol-util/rx-event-helper';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { Canvas3DProps } from 'mol-canvas3d/canvas3d';
 import { PluginCommands } from './command';
 import { PluginCommands } from './command';
+import { PluginAnimationManager } from './state/animation/manager';
 export { PluginState }
 export { PluginState }
 
 
 class PluginState {
 class PluginState {
@@ -20,8 +21,8 @@ class PluginState {
 
 
     readonly dataState: State;
     readonly dataState: State;
     readonly behaviorState: State;
     readonly behaviorState: State;
+    readonly animation: PluginAnimationManager;
     readonly cameraSnapshots = new CameraSnapshotManager();
     readonly cameraSnapshots = new CameraSnapshotManager();
-
     readonly snapshots = new PluginStateSnapshotManager();
     readonly snapshots = new PluginStateSnapshotManager();
 
 
     readonly behavior = {
     readonly behavior = {
@@ -43,6 +44,7 @@ class PluginState {
         return {
         return {
             data: this.dataState.getSnapshot(),
             data: this.dataState.getSnapshot(),
             behaviour: this.behaviorState.getSnapshot(),
             behaviour: this.behaviorState.getSnapshot(),
+            animation: this.animation.getSnapshot(),
             cameraSnapshots: this.cameraSnapshots.getStateSnapshot(),
             cameraSnapshots: this.cameraSnapshots.getStateSnapshot(),
             canvas3d: {
             canvas3d: {
                 camera: this.plugin.canvas3d.camera.getSnapshot(),
                 camera: this.plugin.canvas3d.camera.getSnapshot(),
@@ -60,6 +62,9 @@ class PluginState {
             if (snapshot.canvas3d.camera) this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
             if (snapshot.canvas3d.camera) this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera);
         }
         }
         this.plugin.canvas3d.requestDraw(true);
         this.plugin.canvas3d.requestDraw(true);
+        if (snapshot.animation) {
+            this.animation.setSnapshot(snapshot.animation);
+        }
     }
     }
 
 
     dispose() {
     dispose() {
@@ -67,11 +72,12 @@ class PluginState {
         this.dataState.dispose();
         this.dataState.dispose();
         this.behaviorState.dispose();
         this.behaviorState.dispose();
         this.cameraSnapshots.dispose();
         this.cameraSnapshots.dispose();
+        this.animation.dispose();
     }
     }
 
 
     constructor(private plugin: import('./context').PluginContext) {
     constructor(private plugin: import('./context').PluginContext) {
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
         this.dataState = State.create(new SO.Root({ }), { globalContext: plugin });
-        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin });
+        this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } });
 
 
         this.dataState.behaviors.currentObject.subscribe(o => {
         this.dataState.behaviors.currentObject.subscribe(o => {
             if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(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.behavior.currentObject.next(this.dataState.behaviors.currentObject.value);
+
+        this.animation = new PluginAnimationManager(plugin);
     }
     }
 }
 }
 
 
@@ -90,6 +98,7 @@ namespace PluginState {
     export interface Snapshot {
     export interface Snapshot {
         data?: State.Snapshot,
         data?: State.Snapshot,
         behaviour?: State.Snapshot,
         behaviour?: State.Snapshot,
+        animation?: PluginAnimationManager.Snapshot,
         cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
         cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
         canvas3d?: {
         canvas3d?: {
             camera?: Camera.Snapshot,
             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 { Camera } from 'mol-canvas3d/camera';
 import { OrderedMap } from 'immutable';
 import { OrderedMap } from 'immutable';
 import { UUID } from 'mol-util';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
+import { PluginComponent } from 'mol-plugin/component';
 
 
 export { CameraSnapshotManager }
 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 = {
     readonly events = {
         changed: this.ev()
         changed: this.ev()
     };
     };
 
 
-    get entries() { return this._entries; }
-
     getEntry(id: string) {
     getEntry(id: string) {
-        return this._entries.get(id);
+        return this.state.entries.get(id);
     }
     }
 
 
     remove(id: string) {
     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();
         this.events.changed.next();
     }
     }
 
 
     add(e: CameraSnapshotManager.Entry) {
     add(e: CameraSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.updateState({ entries: this.state.entries.set(e.id, e) });
         this.events.changed.next();
         this.events.changed.next();
     }
     }
 
 
     clear() {
     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();
         this.events.changed.next();
     }
     }
 
 
     getStateSnapshot(): CameraSnapshotManager.StateSnapshot {
     getStateSnapshot(): CameraSnapshotManager.StateSnapshot {
         const entries: CameraSnapshotManager.Entry[] = [];
         const entries: CameraSnapshotManager.Entry[] = [];
-        this._entries.forEach(e => entries.push(e!));
+        this.state.entries.forEach(e => entries.push(e!));
         return { entries };
         return { entries };
     }
     }
 
 
     setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) {
     setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) {
-        this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
+        const entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable();
         for (const e of state.entries) {
         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();
         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 { Representation } from 'mol-repr/representation';
 import { StructureRepresentation } from 'mol-repr/structure/representation';
 import { StructureRepresentation } from 'mol-repr/structure/representation';
 import { VolumeRepresentation } from 'mol-repr/volume/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 { Ccp4File } from 'mol-io/reader/ccp4/schema';
 import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 import { Dsn6File } from 'mol-io/reader/dsn6/schema';
 
 
@@ -77,6 +77,6 @@ export namespace PluginStateObject {
 }
 }
 
 
 export namespace PluginStateTransform {
 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 { OrderedMap } from 'immutable';
 import { UUID } from 'mol-util';
 import { UUID } from 'mol-util';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { PluginState } from '../state';
 import { PluginState } from '../state';
+import { PluginComponent } from 'mol-plugin/component';
 
 
 export { PluginStateSnapshotManager }
 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 = {
     readonly events = {
         changed: this.ev()
         changed: this.ev()
     };
     };
 
 
-    get entries() { return this._entries; }
-
     getEntry(id: string) {
     getEntry(id: string) {
-        return this._entries.get(id);
+        return this.state.entries.get(id);
     }
     }
 
 
     remove(id: string) {
     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();
         this.events.changed.next();
     }
     }
 
 
     add(e: PluginStateSnapshotManager.Entry) {
     add(e: PluginStateSnapshotManager.Entry) {
-        this._entries.set(e.id, e);
+        this.updateState({ entries: this.state.entries.set(e.id, e) });
         this.events.changed.next();
         this.events.changed.next();
     }
     }
 
 
     clear() {
     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();
         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 Data from './transforms/data'
 import * as Model from './transforms/model'
 import * as Model from './transforms/model'
+import * as Volume from './transforms/volume'
 import * as Representation from './transforms/representation'
 import * as Representation from './transforms/representation'
 
 
 export const StateTransforms = {
 export const StateTransforms = {
     Data,
     Data,
     Model,
     Model,
+    Volume,
     Representation
     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 CIF from 'mol-io/reader/cif'
 import { PluginContext } from 'mol-plugin/context';
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 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 { readFromFile } from 'mol-util/data-source';
 import * as CCP4 from 'mol-io/reader/ccp4/parser'
 import * as CCP4 from 'mol-io/reader/ccp4/parser'
 import * as DSN6 from 'mol-io/reader/dsn6/parser'
 import * as DSN6 from 'mol-io/reader/dsn6/parser'
@@ -38,12 +38,12 @@ const Download = PluginStateTransform.BuiltIn({
         });
         });
     },
     },
     update({ oldParams, newParams, b }) {
     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) {
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || newParams.url;
             (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 }) {
     update({ oldParams, newParams, b }) {
         if (oldParams.label !== newParams.label) {
         if (oldParams.label !== newParams.label) {
             (b.label as string) = newParams.label || oldParams.file.name;
             (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.' })
     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>
  * @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 Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
 import { compile } from 'mol-script/runtime/query/compiler';
-import { MolScriptBuilder } from 'mol-script/language/builder';
 import { StateObject } from 'mol-state';
 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 { 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
 type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
 const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
     name: 'trajectory-from-mmcif',
@@ -59,11 +63,10 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
 });
 });
 
 
 
 
-export { TrajectoryFromPDB }
 type TrajectoryFromPDB = typeof TrajectoryFromPDB
 type TrajectoryFromPDB = typeof TrajectoryFromPDB
 const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
 const TrajectoryFromPDB = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-pdb',
     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],
     from: [SO.Data.String],
     to: SO.Molecule.Trajectory
     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;
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
 type ModelFromTrajectory = typeof ModelFromTrajectory
 type ModelFromTrajectory = typeof ModelFromTrajectory
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
 const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     name: 'model-from-trajectory',
     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,
     from: SO.Molecule.Trajectory,
     to: SO.Molecule.Model,
     to: SO.Molecule.Model,
     params: a => {
     params: a => {
@@ -98,12 +100,13 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     apply({ a, params }) {
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         const model = a.data[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);
         return new SO.Molecule.Model(model, props);
     }
     }
 });
 });
 
 
-export { StructureFromModel }
 type StructureFromModel = typeof StructureFromModel
 type StructureFromModel = typeof StructureFromModel
 const StructureFromModel = PluginStateTransform.BuiltIn({
 const StructureFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-from-model',
     name: 'structure-from-model',
@@ -122,7 +125,6 @@ function structureDesc(s: Structure) {
     return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
     return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`;
 }
 }
 
 
-export { StructureAssemblyFromModel }
 type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
 type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
 const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
 const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-assembly-from-model',
     name: 'structure-assembly-from-model',
@@ -143,17 +145,30 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
         return Task.create('Build Assembly', async ctx => {
         return Task.create('Build Assembly', async ctx => {
             const model = a.data;
             const model = a.data;
             let id = params.id;
             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);
             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) };
                 const label = { label: a.data.label, description: structureDesc(base) };
                 return new SO.Molecule.Structure(base, label);
                 return new SO.Molecule.Structure(base, label);
             }
             }
 
 
-            asm = model.symmetry.assemblies[0];
             id = asm.id;
             id = asm.id;
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
             const props = { label: `Assembly ${id}`, description: structureDesc(s) };
             const props = { label: `Assembly ${id}`, description: structureDesc(s) };
@@ -162,7 +177,6 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     }
     }
 });
 });
 
 
-export { StructureSymmetryFromModel }
 type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
 type StructureSymmetryFromModel = typeof StructureSymmetryFromModel
 const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
 const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-symmetry-from-model',
     name: 'structure-symmetry-from-model',
@@ -188,7 +202,6 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({
     }
     }
 });
 });
 
 
-export { StructureSelection }
 type StructureSelection = typeof StructureSelection
 type StructureSelection = typeof StructureSelection
 const StructureSelection = PluginStateTransform.BuiltIn({
 const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
     name: 'structure-selection',
@@ -210,7 +223,6 @@ const StructureSelection = PluginStateTransform.BuiltIn({
     }
     }
 });
 });
 
 
-export { StructureComplexElement }
 namespace StructureComplexElement {
 namespace StructureComplexElement {
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
     export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres'
 }
 }
@@ -243,7 +255,6 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({
     }
     }
 });
 });
 
 
-export { CustomModelProperties }
 type CustomModelProperties = typeof CustomModelProperties
 type CustomModelProperties = typeof CustomModelProperties
 const CustomModelProperties = PluginStateTransform.BuiltIn({
 const CustomModelProperties = PluginStateTransform.BuiltIn({
     name: 'custom-model-properties',
     name: 'custom-model-properties',
@@ -267,83 +278,4 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon
         const p = ctx.customModelProperties.get(name);
         const p = ctx.customModelProperties.get(name);
         await p.attach(model).runInContext(taskCtx);
         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>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
-import { Transformer } from 'mol-state';
+import { StateTransformer } from 'mol-state';
 import { Task } from 'mol-task';
 import { Task } from 'mol-task';
 import { PluginStateTransform } from '../objects';
 import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } 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 { Structure } from 'mol-model/structure';
 import { StructureParams } from 'mol-repr/structure/representation';
 import { StructureParams } from 'mol-repr/structure/representation';
 import { ExplodeRepresentation3D } from 'mol-plugin/behavior/dynamic/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 { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry';
 import { VolumeParams } from 'mol-repr/volume/representation';
 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 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 type = ctx.structureRepresentation.registry.get(name);
 
 
         const themeDataCtx = { structure };
         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 type = ctx.structureRepresentation.registry.get(name);
         const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
         const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
         const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).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) {
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
         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 }
             const props = { ...b.data.props, ...newParams.type.params }
             b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams))
             b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams))
             await b.data.createOrUpdate(props, a.data).runInContext(ctx);
             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 () => {
         return Task.create('Update Explosion', async () => {
             const updated = await b.data.update(newParams);
             const updated = await b.data.update(newParams);
             b.label = `Explosion T = ${newParams.t.toFixed(2)}`;
             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 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 type = ctx.volumeRepresentation.registry.get(name);
 
 
         const themeDataCtx = { volume };
         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 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 ({
         return ({
             type: { name, params: volumeParams ? { ...type.defaultValues, ...volumeParams } : type.defaultValues },
             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 }
 export { VolumeRepresentation3D }
 type VolumeRepresentation3D = typeof VolumeRepresentation3D
 type VolumeRepresentation3D = typeof VolumeRepresentation3D
@@ -189,16 +195,16 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
                 type: PD.Mapped<any>(
                 type: PD.Mapped<any>(
                     registry.default.name,
                     registry.default.name,
                     registry.types,
                     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>(
                 colorTheme: PD.Mapped<any>(
                     type.defaultColorTheme,
                     type.defaultColorTheme,
                     themeCtx.colorThemeRegistry.types,
                     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>(
                 sizeTheme: PD.Mapped<any>(
                     type.defaultSizeTheme,
                     type.defaultSizeTheme,
                     themeCtx.sizeThemeRegistry.types,
                     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)
             const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams)
             repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
             repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params))
             // TODO set initial state, repr.setState({})
             // 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);
             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) {
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Volume Representation', async ctx => {
         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 }
             const props = { ...b.data.props, ...newParams.type.params }
             b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
             b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams))
             await b.data.createOrUpdate(props, a.data).runInContext(ctx);
             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 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;
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
     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;
     static contextType = PluginReactContext;
     readonly plugin: PluginContext;
     readonly plugin: PluginContext;
 
 

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

@@ -6,11 +6,11 @@
 
 
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { ParameterControls } from './controls/parameters';
 
 
-export class CameraSnapshots extends PluginComponent<{ }, { }> {
+export class CameraSnapshots extends PluginUIComponent<{ }, { }> {
     render() {
     render() {
         return <div>
         return <div>
             <div className='msp-section-header'>Camera Snapshots</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 = {
     static Params = {
         name: PD.Text(),
         name: PD.Text(),
         description: 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() {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate());
         this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate());
     }
     }
@@ -65,7 +65,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> {
 
 
     render() {
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
         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 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'>
                 <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
                     <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 * as React from 'react';
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginCommands } from 'mol-plugin/command';
-import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
-import { PluginComponent } from './base';
+import { UpdateTrajectory } from 'mol-plugin/state/actions/structure';
+import { PluginUIComponent } from './base';
 import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager';
 import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager';
 
 
-export class Controls extends PluginComponent<{ }, { }> {
+export class Controls extends PluginUIComponent<{ }, { }> {
     render() {
     render() {
         return <>
         return <>
 
 
@@ -18,7 +18,7 @@ export class Controls extends PluginComponent<{ }, { }> {
     }
     }
 }
 }
 
 
-export class TrajectoryControls extends PluginComponent {
+export class TrajectoryControls extends PluginUIComponent {
     render() {
     render() {
         return <div>
         return <div>
             <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, {
             <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: [] }
     state = { entries: [] }
 
 
     componentDidMount() {
     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: {
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,
 //     onChange: (v: boolean) => void,
 //     value: boolean,
 //     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 * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
 import { Slider, Slider2 } from './slider';
+import { NumericInput } from './common';
 
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     params: P,
@@ -28,15 +29,17 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
     render() {
     render() {
         const params = this.props.params;
         const params = this.props.params;
         const values = this.props.values;
         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];
                 const param = params[key];
                 if (param.isHidden) return null;
                 if (param.isHidden) return null;
                 const Control = controlFor(param);
                 const Control = controlFor(param);
                 if (!Control) return null;
                 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]} />
                 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' };
     state = { value: '0' };
 
 
-    protected update(value: any) {
+    update = (value: number) => {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
         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() {
     render() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         const label = this.props.param.label || camelCaseToWords(this.props.name);
         return <div className='msp-control-row'>
         return <div className='msp-control-row'>
             <span title={this.props.param.description}>{label}</span>
             <span title={this.props.param.description}>{label}</span>
             <div>
             <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>
         </div>;
         </div>;
     }
     }
@@ -208,7 +180,7 @@ export class NumberRangeControl extends SimpleParam<PD.Numeric> {
     onChange = (v: number) => { this.update(v); }
     onChange = (v: number) => { this.update(v); }
     renderControl() {
     renderControl() {
         return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
         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); }
     onChange = (v: [number, number]) => { this.update(v); }
     renderControl() {
     renderControl() {
         return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
         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() {
     render() {
         const params = this.props.param.params;
         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 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} />;
         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 * as React from 'react'
+import { NumericInput } from './common';
+import { noop } from 'mol-util';
 
 
 export class Slider extends React.Component<{
 export class Slider extends React.Component<{
     min: number,
     min: number,
@@ -12,7 +14,8 @@ export class Slider extends React.Component<{
     value: number,
     value: number,
     step?: number,
     step?: number,
     onChange: (v: number) => void,
     onChange: (v: number) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: number }> {
 }, { isChanging: boolean, current: number }> {
 
 
     state = { isChanging: false, current: 0 }
     state = { isChanging: false, current: 0 }
@@ -35,18 +38,35 @@ export class Slider extends React.Component<{
         this.setState({ current });
         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() {
     render() {
         let step = this.props.step;
         let step = this.props.step;
         if (step === void 0) step = 1;
         if (step === void 0) step = 1;
         return <div className='msp-slider'>
         return <div className='msp-slider'>
             <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} />
-                </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>
             <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>
         </div>;
         </div>;
     }
     }
@@ -58,7 +78,8 @@ export class Slider2 extends React.Component<{
     value: [number, number],
     value: [number, number],
     step?: number,
     step?: number,
     onChange: (v: [number, number]) => void,
     onChange: (v: [number, number]) => void,
-    disabled?: boolean
+    disabled?: boolean,
+    onEnter?: () => void
 }, { isChanging: boolean, current: [number, number] }> {
 }, { isChanging: boolean, current: [number, number] }> {
 
 
     state = { isChanging: false, current: [0, 1] as [number, number] }
     state = { isChanging: false, current: [0, 1] as [number, number] }
@@ -81,20 +102,41 @@ export class Slider2 extends React.Component<{
         this.setState({ current });
         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() {
     render() {
         let step = this.props.step;
         let step = this.props.step;
         if (step === void 0) step = 1;
         if (step === void 0) step = 1;
         return <div className='msp-slider2'>
         return <div className='msp-slider2'>
             <div>
             <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>
             <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>
             <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>
         </div>;
         </div>;
     }
     }
@@ -102,10 +144,10 @@ export class Slider2 extends React.Component<{
 
 
 /**
 /**
  * The following code was adapted from react-components/slider library.
  * The following code was adapted from react-components/slider library.
- * 
+ *
  * The MIT License (MIT)
  * The MIT License (MIT)
  * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
  * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
- * 
+ *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * of this software and associated documentation files (the "Software"), to deal
  * of this software and associated documentation files (the "Software"), to deal
  * in the Software without restriction, including without limitation the rights
  * 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
  * The above copyright notice and this permission notice shall be included in
  * all copies or substantial portions of the Software.
  * 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.
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  */
  */
 
 
@@ -151,9 +193,6 @@ function classNames(_classes: { [name: string]: boolean | number }) {
     return classes.join(' ');
     return classes.join(' ');
 }
 }
 
 
-function noop() {
-}
-
 function isNotTouchEvent(e: TouchEvent) {
 function isNotTouchEvent(e: TouchEvent) {
     return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0);
     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 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])
         //     //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,
             dragging: handle === i,
             index: i,
             index: i,
             key: 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(); }
         if (!range) { handles.shift(); }
 
 

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

@@ -4,22 +4,22 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  * @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 * as React from 'react';
 import { PluginContext } from '../context';
 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 { CameraSnapshots } from './camera';
+import { Controls, LociLabelControl, TrajectoryControls } from './controls';
 import { StateSnapshots } from './state';
 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 { 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 }, {}> {
 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() {
     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) {
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -52,7 +52,7 @@ class Layout extends PluginComponent {
     }
     }
 
 
     render() {
     render() {
-        const layout = this.plugin.layout.latestState;
+        const layout = this.plugin.layout.state;
         return <div className='msp-plugin'>
         return <div className='msp-plugin'>
             <div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}>
             <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'}>
                 <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'>
                     {layout.showControls && this.region('right', <div className='msp-scrollable-container msp-right-controls'>
                         <CurrentObject />
                         <CurrentObject />
                         <Controls />
                         <Controls />
+                        <AnimationControls />
                         <CameraSnapshots />
                         <CameraSnapshots />
                         <StateSnapshots />
                         <StateSnapshots />
                     </div>)}
                     </div>)}
@@ -71,7 +72,7 @@ class Layout extends PluginComponent {
     }
     }
 }
 }
 
 
-export class ViewportWrapper extends PluginComponent {
+export class ViewportWrapper extends PluginUIComponent {
     render() {
     render() {
         return <>
         return <>
             <Viewport />
             <Viewport />
@@ -89,7 +90,7 @@ export class ViewportWrapper extends PluginComponent {
     }
     }
 }
 }
 
 
-export class State extends PluginComponent {
+export class State extends PluginUIComponent {
     componentDidMount() {
     componentDidMount() {
         this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate());
         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;
         const kind = this.plugin.state.behavior.kind.value;
         return <div className='msp-scrollable-container'>
         return <div className='msp-scrollable-container'>
             <div className='msp-btn-row-group msp-data-beh'>
             <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>
             </div>
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
         </div>
         </div>
     }
     }
 }
 }
 
 
-export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
+export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
     private wrapper = React.createRef<HTMLDivElement>();
 
 
     componentDidMount() {
     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() {
     componentDidUpdate() {
         this.scrollToBottom();
         this.scrollToBottom();
     }
     }
 
 
-    state = { entries: this.plugin.log.entries.takeLast(100).toList() };
+    state = { entries: this.plugin.log.entries };
 
 
     private scrollToBottom() {
     private scrollToBottom() {
         const log = this.wrapper.current;
         const log = this.wrapper.current;
@@ -130,19 +131,26 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     }
     }
 
 
     render() {
     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' }}>
         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>;
         </div>;
     }
     }
 }
 }
 
 
-export class CurrentObject extends PluginComponent {
+export class CurrentObject extends PluginUIComponent {
     get current() {
     get current() {
         return this.plugin.state.behavior.currentObject.value;
         return this.plugin.state.behavior.currentObject.value;
     }
     }
@@ -163,21 +171,21 @@ export class CurrentObject extends PluginComponent {
         const current = this.current;
         const current = this.current;
         const ref = current.ref;
         const ref = current.ref;
         const cell = current.state.cells.get(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 transform = cell.transform;
         const def = transform.transformer.definition;
         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 { PluginCommands } from 'mol-plugin/command';
 import * as React from 'react';
 import * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { shallowEqual } from 'mol-util';
 import { shallowEqual } from 'mol-util';
 import { List } from 'immutable';
 import { List } from 'immutable';
 import { ParameterControls } from './controls/parameters';
 import { ParameterControls } from './controls/parameters';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 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' }
     state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' }
 
 
     updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) };
     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.
 // TODO: this is not nice: device some custom event system.
 const UploadedEvent = new Subject();
 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 };
     state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false };
 
 
     static Params = {
     static Params = {
@@ -93,7 +93,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC
     }
     }
 }
 }
 
 
-class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
+class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> {
     componentDidMount() {
     componentDidMount() {
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
         this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate());
     }
     }
@@ -110,7 +110,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> {
 
 
     render() {
     render() {
         return <ul style={{ listStyle: 'none' }} className='msp-state-list'>
         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 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'>
                 <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'>
                     <span className='msp-icon msp-icon-remove' />
                     <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 }
 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 };
     state = { entries: List<RemoteEntry>(), isFetching: false };
 
 
     componentDidMount() {
     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 { PluginCommands } from 'mol-plugin/command';
 import { PluginContext } from 'mol-plugin/context';
 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 { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
 import { StateTransformParameters, TransformContolBase } from './common';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -17,13 +16,13 @@ export { ApplyActionContol };
 namespace ApplyActionContol {
 namespace ApplyActionContol {
     export interface Props {
     export interface Props {
         plugin: PluginContext,
         plugin: PluginContext,
-        nodeRef: Transform.Ref,
+        nodeRef: StateTransform.Ref,
         state: State,
         state: State,
         action: StateAction
         action: StateAction
     }
     }
 
 
     export interface ComponentState {
     export interface ComponentState {
-        ref: Transform.Ref,
+        ref: StateTransform.Ref,
         version: string,
         version: string,
         params: any,
         params: any,
         error?: string,
         error?: string,
@@ -48,7 +47,7 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
     applyText() { return 'Apply'; }
     applyText() { return 'Apply'; }
     isUpdate() { return false; }
     isUpdate() { return false; }
 
 
-    private _getInfo = memoizeLatest((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
+    private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 
 
     state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
     state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false };
 
 

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

@@ -4,18 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  * @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 * as React from 'react';
-import { PurePluginComponent } from '../base';
+import { PurePluginUIComponent } from '../base';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
-import { StateAction } from 'mol-state/action';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 import { Subject } from 'rxjs';
 
 
 export { StateTransformParameters, TransformContolBase };
 export { StateTransformParameters, TransformContolBase };
 
 
-class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> {
+class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> {
     validate(params: any) {
     validate(params: any) {
         // TODO
         // TODO
         return void 0;
         return void 0;
@@ -61,7 +60,7 @@ namespace StateTransformParameters {
         return true;
         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 source = state.cells.get(nodeRef)!.obj!;
         const params = action.definition.params ? action.definition.params(source, plugin) : { };
         const params = action.definition.params ? action.definition.params(source, plugin) : { };
         const initialValues = PD.getDefaultValues(params);
         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 cell = state.cells.get(transform.ref)!;
         // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
         // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0;
         // const create = transform.transformer.definition.params;
         // const create = transform.transformer.definition.params;
@@ -88,7 +87,7 @@ namespace StateTransformParameters {
 }
 }
 
 
 namespace TransformContolBase {
 namespace TransformContolBase {
-    export interface ControlState {
+    export interface ComponentState {
         params: any,
         params: any,
         error?: string,
         error?: string,
         busy: boolean,
         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 applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
     abstract getInfo(): StateTransformParameters.Props['info'];
-    abstract getHeader(): Transformer.Definition['display'];
+    abstract getHeader(): StateTransformer.Definition['display'];
     abstract canApply(): boolean;
     abstract canApply(): boolean;
     abstract getTransformerId(): string;
     abstract getTransformerId(): string;
     abstract canAutoApply(newParams: any): boolean;
     abstract canAutoApply(newParams: any): boolean;
@@ -167,7 +166,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
 
 
     render() {
     render() {
         const info = this.getInfo();
         const info = this.getInfo();
-        if (info.isEmpty && this.isUpdate()) return null;
+        const isEmpty = info.isEmpty && this.isUpdate();
 
 
         const display = this.getHeader();
         const display = this.getHeader();
 
 
@@ -176,17 +175,26 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
             ? this.plugin.customParamEditors.get(tId)!
             ? this.plugin.customParamEditors.get(tId)!
             : StateTransformParameters;
             : 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'>
             <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>
             </div>
-            {!this.state.isCollapsed && <>
+            {!isEmpty && !this.state.isCollapsed && <>
                 <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
                 <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
 
 
                 <div className='msp-transform-apply-wrap'>
                 <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}>
                     <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>
                     </button>
                     <div className='msp-transform-apply'>
                     <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()}>
                         <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>
  * @author David Sehnal <david.sehnal@gmail.com>
  */
  */
 
 
 import * as React from 'react';
 import * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
 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 { 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() {
     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) {
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
     }
@@ -60,24 +86,37 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, {
     }
     }
 
 
     render() {
     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 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);
         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
             {children.size === 0
                 ? void 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>
             }
             }
-        </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) {
     is(e: State.ObjectEvent) {
         return e.ref === this.props.nodeRef && e.state === this.props.state;
         return e.ref === this.props.nodeRef && e.state === this.props.state;
     }
     }
@@ -107,7 +146,8 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
     state = {
     state = {
         isCurrent: this.props.state.current === this.props.nodeRef,
         isCurrent: this.props.state.current === this.props.nodeRef,
         isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
         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 }) {
     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 {
         return {
             isCurrent: props.state.current === props.nodeRef,
             isCurrent: props.state.current === props.nodeRef,
             isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
             isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
+            state: props.state,
+            updaterCollapsed: true
         };
         };
     }
     }
 
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         e.preventDefault();
+        e.currentTarget.blur();
         PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
         PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
     }
     }
 
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         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>) => {
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
@@ -153,6 +195,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         e.currentTarget.blur();
         e.currentTarget.blur();
     }
     }
 
 
+    private toggleUpdaterObs = new Subject();
+    toggleUpdater = (e: React.MouseEvent<HTMLAnchorElement>) => {
+        e.preventDefault();
+        e.currentTarget.blur();
+        this.toggleUpdaterObs.next();
+    }
+
     render() {
     render() {
         const n = this.props.state.transforms.get(this.props.nodeRef)!;
         const n = this.props.state.transforms.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
@@ -171,26 +220,63 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         } else {
         } else {
             const obj = cell.obj as PluginStateObject.Any;
             const obj = cell.obj as PluginStateObject.Any;
             const title = `${obj.label} ${obj.description ? obj.description : ''}`
             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 children = this.props.state.tree.children.get(this.props.nodeRef);
         const cellState = this.props.state.cellStates.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' : ''}`}>
         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' />
             <span className='msp-icon msp-icon-visual-visibility' />
         </button>;
         </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'>
             {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'}`} />
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
             </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>
  * @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 { memoizeLatest } from 'mol-util/memoize';
 import { StateTransformParameters, TransformContolBase } from './common';
 import { StateTransformParameters, TransformContolBase } from './common';
+import { Observable } from 'rxjs';
 
 
 export { UpdateTransformContol };
 export { UpdateTransformContol };
 
 
 namespace UpdateTransformContol {
 namespace UpdateTransformContol {
     export interface Props {
     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);
         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) {
     static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) {
         if (props.transform === state.transform) return null;
         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 * as React from 'react';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { OrderedMap } from 'immutable';
 import { OrderedMap } from 'immutable';
 import { TaskManager } from 'mol-plugin/util/task-manager';
 import { TaskManager } from 'mol-plugin/util/task-manager';
 import { filter } from 'rxjs/operators';
 import { filter } from 'rxjs/operators';
 import { Progress } from 'mol-task';
 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() {
     componentDidMount() {
         this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => {
         this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => {
             this.setState({ tracked: this.state.tracked.set(e.id, 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() {
     render() {
         const root = this.props.event.progress.root;
         const root = this.props.event.progress.root;
         const subtaskCount = countSubtasks(this.props.event.progress.root) - 1;
         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 * as React from 'react';
 import { ButtonsType } from 'mol-util/input/input-observer';
 import { ButtonsType } from 'mol-util/input/input-observer';
 import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify';
 import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify';
-import { PluginComponent } from './base';
+import { PluginUIComponent } from './base';
 import { PluginCommands } from 'mol-plugin/command';
 import { PluginCommands } from 'mol-plugin/command';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
 import { ParameterControls } from './controls/parameters';
@@ -20,7 +20,7 @@ interface ViewportState {
     noWebGl: boolean
     noWebGl: boolean
 }
 }
 
 
-export class ViewportControls extends PluginComponent {
+export class ViewportControls extends PluginUIComponent {
     state = {
     state = {
         isSettingsExpanded: false
         isSettingsExpanded: false
     }
     }
@@ -35,11 +35,11 @@ export class ViewportControls extends PluginComponent {
     }
     }
 
 
     toggleControls = () => {
     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 = () => {
     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 }) => {
     setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
@@ -55,7 +55,7 @@ export class ViewportControls extends PluginComponent {
             this.forceUpdate();
             this.forceUpdate();
         });
         });
 
 
-        this.subscribe(this.plugin.layout.updated, () => {
+        this.subscribe(this.plugin.layout.events.updated, () => {
             this.forceUpdate();
             this.forceUpdate();
         });
         });
     }
     }
@@ -73,15 +73,15 @@ export class ViewportControls extends PluginComponent {
         // TODO: show some icons dimmed etc..
         // TODO: show some icons dimmed etc..
         return <div className={'msp-viewport-controls'}>
         return <div className={'msp-viewport-controls'}>
             <div className='msp-viewport-controls-buttons'>
             <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('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)}
                 {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}
                 {this.icon('reset-scene', this.resetCamera, 'Reset Camera')}
             </div>
             </div>
             {this.state.isSettingsExpanded &&
             {this.state.isSettingsExpanded &&
             <div className='msp-viewport-controls-scene-options'>
             <div className='msp-viewport-controls-scene-options'>
                 <ControlGroup header='Layout' initialExpanded={true}>
                 <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>
                 <ControlGroup header='Viewport' initialExpanded={true}>
                 <ControlGroup header='Viewport' initialExpanded={true}>
                     <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
                     <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 container = React.createRef<HTMLDivElement>();
     private canvas = React.createRef<HTMLCanvasElement>();
     private canvas = React.createRef<HTMLCanvasElement>();
 
 
@@ -128,7 +128,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> {
             idHelper.select(x, y);
             idHelper.select(x, y);
         });
         });
 
 
-        this.subscribe(this.plugin.layout.updated, () => {
+        this.subscribe(this.plugin.layout.events.updated, () => {
             setTimeout(this.handleResize, 50);
             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 => {
     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.prevT = t;
             this.currentIdentifyT = t;
             this.currentIdentifyT = t;
             this.identify(false, t);
             this.identify(false, t);

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

@@ -188,11 +188,7 @@ namespace Representation {
                 }
                 }
                 return renderObjects
                 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 },
             get params() { return currentParams },
             createOrUpdate: (props: Partial<P> = {}, data?: D) => {
             createOrUpdate: (props: Partial<P> = {}, data?: D) => {
                 if (data && data !== currentData) {
                 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 { NullLocation } from 'mol-model/location';
 import { Lines } from 'mol-geo/geometry/lines/lines';
 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
 type IsoValueParam = typeof IsoValueParam
 
 
 export const VolumeIsosurfaceParams = {
 export const VolumeIsosurfaceParams = {
@@ -50,7 +54,7 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
     ctx.runtime.update({ message: 'Marching cubes...' });
     ctx.runtime.update({ message: 'Marching cubes...' });
 
 
     const surface = await computeMarchingCubesMesh({
     const surface = await computeMarchingCubesMesh({
-        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue,
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
         scalarField: volume.data
         scalarField: volume.data
     }, mesh).runAsChild(ctx.runtime);
     }, mesh).runAsChild(ctx.runtime);
 
 
@@ -88,7 +92,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
     ctx.runtime.update({ message: 'Marching cubes...' });
     ctx.runtime.update({ message: 'Marching cubes...' });
 
 
     const wireframe = await computeMarchingCubesLines({
     const wireframe = await computeMarchingCubesLines({
-        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue,
+        isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue,
         scalarField: volume.data
         scalarField: volume.data
     }, lines).runAsChild(ctx.runtime)
     }, lines).runAsChild(ctx.runtime)
 
 
@@ -135,24 +139,26 @@ export const IsosurfaceParams = {
 export type IsosurfaceParams = typeof IsosurfaceParams
 export type IsosurfaceParams = typeof IsosurfaceParams
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
 export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) {
     const p = PD.clone(IsosurfaceParams)
     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(
     p.isoValue = PD.Conditioned(
-        VolumeIsoValue.relative(volume.dataStats, 2),
+        VolumeIsoValue.relative(2),
         {
         {
             'absolute': PD.Converted(
             '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 })
                 PD.Numeric(mean, { min, max, step: sigma / 100 })
             ),
             ),
             'relative': PD.Converted(
             '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) => 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
     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 { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateObject, StateObjectCell } from './object';
 import { StateObject, StateObjectCell } from './object';
 import { State } from './state';
 import { State } from './state';
-import { Transformer } from './transformer';
+import { StateTransformer } from './transformer';
+import { StateTransform } from './transform';
 
 
 export { StateAction };
 export { StateAction };
 
 
@@ -45,7 +46,7 @@ namespace StateAction {
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
 
         /** Test if the transform can be applied to a given node */
         /** 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> {
     export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
@@ -63,12 +64,15 @@ namespace StateAction {
         return action;
         return action;
     }
     }
 
 
-    export function fromTransformer<T extends Transformer>(transformer: T) {
+    export function fromTransformer<T extends StateTransformer>(transformer: T) {
         const def = transformer.definition;
         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,
             from: def.from,
             display: def.display,
             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 }) {
             run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 return state.updateTree(tree) as Task<void>;
                 return state.updateTree(tree) as Task<void>;
@@ -80,7 +84,8 @@ namespace StateAction {
         export interface Type<A extends StateObject.Ctor, P extends { }> {
         export interface Type<A extends StateObject.Ctor, P extends { }> {
             from?: A | A[],
             from?: A | A[],
             params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
             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 {
         export interface Root {
@@ -106,6 +111,7 @@ namespace StateAction {
                     : !!info.params
                     : !!info.params
                     ? info.params as any
                     ? info.params as any
                     : void 0,
                     : void 0,
+                isApplicable: info.isApplicable,
                 ...(typeof def === 'function'
                 ...(typeof def === 'function'
                     ? { run: def }
                     ? { run: def }
                     : def)
                     : def)

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

@@ -4,9 +4,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  * @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 }
 export { StateActionManager }
 
 
@@ -14,8 +14,8 @@ class StateActionManager {
     private actions: Map<StateAction['id'], StateAction> = new Map();
     private actions: Map<StateAction['id'], StateAction> = new Map();
     private fromTypeIndex = new Map<StateObject.Type, StateAction[]>();
     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;
         if (this.actions.has(action.id)) return this;
 
 
@@ -32,7 +32,31 @@ class StateActionManager {
         return this;
         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 './object'
+export * from './tree'
 export * from './state'
 export * from './state'
+export * from './state/builder'
+export * from './state/selection'
 export * from './transformer'
 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 { UUID } from 'mol-util';
-import { Transform } from './transform';
+import { StateTransform } from './transform';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { State } from './state';
 import { State } from './state';
-import { StateSelection } from './state/selection';
+import { StateSelection } from 'mol-state';
 
 
 export { StateObject, StateObjectCell }
 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
     // 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,
     status: StateObjectCell.Status,
 
 
     params: {
     params: {
@@ -68,7 +67,7 @@ interface StateObjectCell {
     } | undefined;
     } | undefined;
 
 
     errorText?: string,
     errorText?: string,
-    obj?: StateObject
+    obj?: T
 }
 }
 
 
 namespace StateObjectCell {
 namespace StateObjectCell {

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

@@ -6,13 +6,12 @@
 
 
 import { StateObject, StateObjectCell } from './object';
 import { StateObject, StateObjectCell } from './object';
 import { StateTree } from './tree';
 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 { RuntimeContext, Task } from 'mol-task';
 import { StateSelection } from './state/selection';
 import { StateSelection } from './state/selection';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
-import { StateTreeBuilder } from './tree/builder';
+import { StateBuilder } from './state/builder';
 import { StateAction } from './action';
 import { StateAction } from './action';
 import { StateActionManager } from './action/manager';
 import { StateActionManager } from './action/manager';
 import { TransientTree } from './tree/transient';
 import { TransientTree } from './tree/transient';
@@ -23,10 +22,10 @@ import { ParamDefinition } from 'mol-util/param-definition';
 export { State }
 export { State }
 
 
 class State {
 class State {
-    private _tree: TransientTree = StateTree.createEmpty().asTransient();
+    private _tree: TransientTree;
 
 
     protected errorFree = true;
     protected errorFree = true;
-    private transformCache = new Map<Transform.Ref, unknown>();
+    private transformCache = new Map<StateTransform.Ref, unknown>();
 
 
     private ev = RxEventHelper.create();
     private ev = RxEventHelper.create();
 
 
@@ -35,7 +34,7 @@ class State {
         cell: {
         cell: {
             stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
             stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
             created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
-            removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(),
+            removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
         },
         },
         object: {
         object: {
             updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
             updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
@@ -47,7 +46,7 @@ class State {
     };
     };
 
 
     readonly behaviors = {
     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();
     readonly actions = new StateActionManager();
@@ -57,7 +56,7 @@ class State {
     get cellStates() { return (this._tree as StateTree).cellStates; }
     get cellStates() { return (this._tree as StateTree).cellStates; }
     get current() { return this.behaviors.currentObject.value.ref; }
     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();
     readonly cells: State.Cells = new Map();
 
 
@@ -70,11 +69,11 @@ class State {
         return this.updateTree(tree);
         return this.updateTree(tree);
     }
     }
 
 
-    setCurrent(ref: Transform.Ref) {
+    setCurrent(ref: StateTransform.Ref) {
         this.behaviors.currentObject.next({ state: this, 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'
         const update = typeof stateOrProvider === 'function'
             ? stateOrProvider(this.tree.cellStates.get(ref))
             ? stateOrProvider(this.tree.cellStates.get(ref))
             : stateOrProvider;
             : stateOrProvider;
@@ -110,7 +109,7 @@ class State {
      * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
      * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
      * If no ref is specified, apply to root.
      * 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 => {
         return Task.create('Apply Action', ctx => {
             const cell = this.cells.get(ref);
             const cell = this.cells.get(ref);
             if (!cell) throw new Error(`'${ref}' does not exist.`);
             if (!cell) throw new Error(`'${ref}' does not exist.`);
@@ -123,19 +122,19 @@ class State {
     /**
     /**
      * Reconcialites the existing state tree with the new version.
      * 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 tree Tree instance or a tree builder instance
      * @param doNotReportTiming Indicates whether to log timing of the individual transforms
      * @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 => {
         return Task.create('Update Tree', async taskCtx => {
             let updated = false;
             let updated = false;
             try {
             try {
                 const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, doNotLogTiming);
                 const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, doNotLogTiming);
                 updated = await update(ctx);
                 updated = await update(ctx);
-                if (StateTreeBuilder.isTo(tree)) {
+                if (StateBuilder.isTo(tree)) {
                     const cell = this.select(tree.ref)[0];
                     const cell = this.select(tree.ref)[0];
                     return cell && cell.obj;
                     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;
         const oldTree = this._tree;
         this._tree = _tree;
         this._tree = _tree;
 
 
         const ctx: UpdateContext = {
         const ctx: UpdateContext = {
             parent: this,
             parent: this,
-            editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
+            editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0,
 
 
             errorFree: this.errorFree,
             errorFree: this.errorFree,
             taskCtx,
             taskCtx,
             oldTree,
             oldTree,
             tree: _tree,
             tree: _tree,
-            cells: this.cells as Map<Transform.Ref, StateObjectCell>,
+            cells: this.cells as Map<StateTransform.Ref, StateObjectCell>,
             transformCache: this.transformCache,
             transformCache: this.transformCache,
 
 
             results: [],
             results: [],
@@ -175,16 +174,16 @@ class State {
         return ctx;
         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 tree = this._tree;
         const root = tree.root;
         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,
             transform: root,
             sourceRef: void 0,
             sourceRef: void 0,
             obj: rootObject,
             obj: rootObject,
             status: 'ok',
             status: 'ok',
-            version: root.version,
             errorText: void 0,
             errorText: void 0,
             params: {
             params: {
                 definition: {},
                 definition: {},
@@ -197,10 +196,10 @@ class State {
 }
 }
 
 
 namespace State {
 namespace State {
-    export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell>
+    export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell>
 
 
     export type Tree = StateTree
     export type Tree = StateTree
-    export type Builder = StateTreeBuilder
+    export type Builder = StateBuilder
 
 
     export interface ObjectEvent {
     export interface ObjectEvent {
         state: State,
         state: State,
@@ -211,22 +210,22 @@ namespace State {
         readonly tree: StateTree.Serialized
         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);
         return new State(rootObject, params);
     }
     }
 }
 }
 
 
-type Ref = Transform.Ref
+type Ref = StateTransform.Ref
 
 
 interface UpdateContext {
 interface UpdateContext {
     parent: State,
     parent: State,
-    editInfo: StateTreeBuilder.EditInfo | undefined
+    editInfo: StateBuilder.EditInfo | undefined
 
 
     errorFree: boolean,
     errorFree: boolean,
     taskCtx: RuntimeContext,
     taskCtx: RuntimeContext,
     oldTree: StateTree,
     oldTree: StateTree,
     tree: TransientTree,
     tree: TransientTree,
-    cells: Map<Transform.Ref, StateObjectCell>,
+    cells: Map<StateTransform.Ref, StateObjectCell>,
     transformCache: Map<Ref, unknown>,
     transformCache: Map<Ref, unknown>,
 
 
     results: UpdateNodeResult[],
     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
     // 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);
     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) {
     if (fastTrack) {
         deletes = [];
         deletes = [];
@@ -309,7 +308,7 @@ async function update(ctx: UpdateContext) {
         await updateSubtree(ctx, root);
         await updateSubtree(ctx, root);
     }
     }
 
 
-    let newCurrent: Transform.Ref | undefined = ctx.newCurrent;
+    let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
     // Raise object updated events
     // Raise object updated events
     for (const update of ctx.results) {
     for (const update of ctx.results) {
         if (update.action === 'created') {
         if (update.action === 'created') {
@@ -342,15 +341,15 @@ async function update(ctx: UpdateContext) {
     return deletes.length > 0 || roots.length > 0 || ctx.changed;
     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 };
     const findState = { roots: [] as Ref[], cells };
     StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
     StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
     return findState.roots;
     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);
     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);
         s.roots.push(n.ref);
         return false;
         return false;
     }
     }
@@ -360,7 +359,7 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells:
 }
 }
 
 
 type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
 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);
     if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
 }
 }
 function findDeletes(ctx: UpdateContext): Ref[] {
 function findDeletes(ctx: UpdateContext): Ref[] {
@@ -369,7 +368,7 @@ function findDeletes(ctx: UpdateContext): Ref[] {
     return deleteCtx.deletes;
     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;
     if (!oldState.has(n.ref)) return;
     (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
     (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) });
     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;
     ctx.cells.get(t.ref)!.transform = t;
     setCellStatus(ctx, t.ref, 'pending');
     setCellStatus(ctx, t.ref, 'pending');
 }
 }
@@ -397,7 +396,7 @@ function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
 }
 }
 
 
 type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] }
 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)) {
     if (ctx.cells.has(transform.ref)) {
         return;
         return;
     }
     }
@@ -406,7 +405,6 @@ function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCell
         transform,
         transform,
         sourceRef: void 0,
         sourceRef: void 0,
         status: 'pending',
         status: 'pending',
-        version: UUID.create22(),
         errorText: void 0,
         errorText: void 0,
         params: 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 {
 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 node = tree.transforms.get(ref)!;
     const siblings = tree.children.get(node.parent)!.values();
     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 prms = transform.transformer.definition.params;
     const definition = prms ? prms(src, ctx.parent.globalContext) : {};
     const definition = prms ? prms(src, ctx.parent.globalContext) : {};
     const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
     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;
     const transform = current.transform;
 
 
     // special case for Root
     // special case for Root
-    if (current.transform.ref === Transform.RootRef) {
-        current.version = transform.version;
+    if (current.transform.ref === StateTransform.RootRef) {
         return { action: 'none' };
         return { action: 'none' };
     }
     }
 
 
@@ -574,7 +571,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
         current.params = params;
         current.params = params;
         const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values);
         const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values);
         current.obj = obj;
         current.obj = obj;
-        current.version = transform.version;
 
 
         return { ref: currentRef, action: 'created', obj };
         return { ref: currentRef, action: 'created', obj };
     } else {
     } else {
@@ -584,21 +580,18 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo
 
 
         const updateKind = !!current.obj && current.obj !== StateObject.Null
         const updateKind = !!current.obj && current.obj !== StateObject.Null
             ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams)
             ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams)
-            : Transformer.UpdateResult.Recreate;
+            : StateTransformer.UpdateResult.Recreate;
 
 
         switch (updateKind) {
         switch (updateKind) {
-            case Transformer.UpdateResult.Recreate: {
+            case StateTransformer.UpdateResult.Recreate: {
                 const oldObj = current.obj;
                 const oldObj = current.obj;
                 const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams);
                 const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams);
                 current.obj = newObj;
                 current.obj = newObj;
-                current.version = transform.version;
                 return { ref: currentRef, action: 'replaced', oldObj, obj: newObj };
                 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! };
                 return { ref: currentRef, action: 'updated', obj: current.obj! };
             default:
             default:
-                current.version = transform.version;
                 return { action: 'none' };
                 return { action: 'none' };
         }
         }
     }
     }
@@ -609,15 +602,15 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
     return t as T;
     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);
     const cache = Object.create(null);
     ctx.transformCache.set(ref, cache);
     ctx.transformCache.set(ref, cache);
     return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx);
     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) {
     if (!transformer.definition.update) {
-        return Transformer.UpdateResult.Recreate;
+        return StateTransformer.UpdateResult.Recreate;
     }
     }
     let cache = ctx.transformCache.get(ref);
     let cache = ctx.transformCache.get(ref);
     if (!cache) {
     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 { StateObject, StateObjectCell } from '../object';
 import { State } from '../state';
 import { State } from '../state';
 import { StateTree } from '../tree';
 import { StateTree } from '../tree';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 
 
 namespace StateSelection {
 namespace StateSelection {
     export type Selector = Query | Builder | string | StateObjectCell;
     export type Selector = Query | Builder | string | StateObjectCell;
@@ -29,7 +29,7 @@ namespace StateSelection {
     }
     }
 
 
     function isObj(arg: any): arg is StateObjectCell {
     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 {
     function isBuilder(arg: any): arg is Builder {
@@ -75,7 +75,7 @@ namespace StateSelection {
     export namespace Generators {
     export namespace Generators {
         export const root = build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]);
         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) => {
             return build(() => (state: State) => {
                 const ret: StateObjectCell[] = [];
                 const ret: StateObjectCell[] = [];
                 for (const ref of refs) {
                 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);
             const cell = s.cells.get(n.ref);
             if (cell && cell.obj && cell.obj.type === s.type) {
             if (cell && cell.obj && cell.obj.type === s.type) {
                 s.roots.push(cell);
                 s.roots.push(cell);
@@ -196,7 +196,7 @@ namespace StateSelection {
     registerModifier('parent', parent);
     registerModifier('parent', parent);
     export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); }
     export function parent(b: Selector) { return unique(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;
         let current = tree.transforms.get(root)!, len = types.length;
         while (true) {
         while (true) {
             current = tree.transforms.get(current.parent)!;
             current = tree.transforms.get(current.parent)!;
@@ -206,13 +206,13 @@ namespace StateSelection {
             for (let i = 0; i < len; i++) {
             for (let i = 0; i < len; i++) {
                 if (obj.type === types[i].type) return cells.get(current.ref);
                 if (obj.type === types[i].type) return cells.get(current.ref);
             }
             }
-            if (current.ref === Transform.RootRef) {
+            if (current.ref === StateTransform.RootRef) {
                 return void 0;
                 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;
         let parent: StateObjectCell | undefined, _root = root;
         while (true) {
         while (true) {
             const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types);
             const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types);

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

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

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

@@ -5,25 +5,28 @@
  */
  */
 
 
 import { Task } from 'mol-task';
 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 { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateAction } from './action';
 import { StateAction } from './action';
 import { capitalize } from 'mol-util/string';
 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>,
     toAction(): StateAction<A, void, P>,
     readonly namespace: string,
     readonly namespace: string,
     readonly id: Transformer.Id,
     readonly id: Transformer.Id,
     readonly definition: Transformer.Definition<A, B, P>
     readonly definition: Transformer.Definition<A, B, P>
 }
 }
 
 
-export namespace Transformer {
+namespace Transformer {
     export type Id = string & { '@type': 'transformer-id' }
     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 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 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 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 {
     export function is(obj: any): obj is Transformer {
         return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function';
         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> = {
         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); },
             toAction() { return StateAction.fromTransformer(t); },
             namespace,
             namespace,
             id,
             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 { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { TransientTree } from './transient';
 import { TransientTree } from './transient';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 import { StateObjectCell } from 'mol-state/object';
 
 
 export { StateTree }
 export { StateTree }
@@ -17,17 +16,16 @@ export { StateTree }
  * Represented as an immutable map.
  * Represented as an immutable map.
  */
  */
 interface StateTree {
 interface StateTree {
-    readonly root: Transform,
+    readonly root: StateTransform,
     readonly transforms: StateTree.Transforms,
     readonly transforms: StateTree.Transforms,
     readonly children: StateTree.Children,
     readonly children: StateTree.Children,
     readonly cellStates: StateTree.CellStates,
     readonly cellStates: StateTree.CellStates,
 
 
-    asTransient(): TransientTree,
-    build(): StateTreeBuilder.Root
+    asTransient(): TransientTree
 }
 }
 
 
 namespace StateTree {
 namespace StateTree {
-    type Ref = Transform.Ref
+    type Ref = StateTransform.Ref
 
 
     export interface ChildSet {
     export interface ChildSet {
         readonly size: number,
         readonly size: number,
@@ -43,21 +41,17 @@ namespace StateTree {
         get(ref: Ref): T
         get(ref: Ref): T
     }
     }
 
 
-    export interface Transforms extends _Map<Transform> {}
+    export interface Transforms extends _Map<StateTransform> {}
     export interface Children extends _Map<ChildSet> { }
     export interface Children extends _Map<ChildSet> { }
     export interface CellStates extends _Map<StateObjectCell.State> { }
     export interface CellStates extends _Map<StateObjectCell.State> { }
 
 
     class Impl implements StateTree {
     class Impl implements StateTree {
-        get root() { return this.transforms.get(Transform.RootRef)! }
+        get root() { return this.transforms.get(StateTransform.RootRef)! }
 
 
         asTransient(): TransientTree {
         asTransient(): TransientTree {
             return new TransientTree(this);
             return new TransientTree(this);
         }
         }
 
 
-        build(): StateTreeBuilder.Root {
-            return new StateTreeBuilder.Root(this);
-        }
-
         constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
         constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) {
         }
         }
     }
     }
@@ -65,8 +59,8 @@ namespace StateTree {
     /**
     /**
      * Create an instance of an immutable tree.
      * 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]]));
         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);
         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 _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);
         const children = ctx.tree.children.get(root.ref);
         if (children && children.size) {
         if (children && children.size) {
             children.forEach(_postOrderFunc, ctx);
             children.forEach(_postOrderFunc, ctx);
@@ -88,14 +82,14 @@ namespace StateTree {
     /**
     /**
      * Visit all nodes in a subtree in "post order", meaning leafs get visited first.
      * 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 };
         const ctx: VisitorCtx = { tree, state, f };
         _doPostOrder(ctx, root);
         _doPostOrder(ctx, root);
         return ctx.state;
         return ctx.state;
     }
     }
 
 
     function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); }
     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);
         const ret = ctx.f(root, ctx.tree, ctx.state);
         if (typeof ret === 'boolean' && !ret) return;
         if (typeof ret === 'boolean' && !ret) return;
         const children = ctx.tree.children.get(root.ref);
         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.
      * 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.
      * 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 };
         const ctx: VisitorCtx = { tree, state, f };
         _doPreOrder(ctx, root);
         _doPreOrder(ctx, root);
         return ctx.state;
         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.
      * 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[] = [];
         // const children: Ref[] = [];
         // tree.children.get(node.ref).forEach(_visitChildToJson as any, children);
         // 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 {
     export interface Serialized {
         /** Transforms serialized in pre-order */
         /** 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);
         doPreOrder(tree, tree.root, transforms, _visitNodeToJson);
         return { transforms };
         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 children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable();
         const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
         const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable();
 
 
         for (const t of data.transforms) {
         for (const t of data.transforms) {
-            const transform = Transform.fromJSON(t[0]);
+            const transform = StateTransform.fromJSON(t[0]);
             nodes.set(transform.ref, transform);
             nodes.set(transform.ref, transform);
             cellStates.set(transform.ref, t[1]);
             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 { Map as ImmutableMap, OrderedSet } from 'immutable';
-import { Transform } from '../transform';
+import { StateTransform } from '../transform';
 import { StateTree } from './immutable';
 import { StateTree } from './immutable';
-import { StateTreeBuilder } from './builder';
 import { StateObjectCell } from 'mol-state/object';
 import { StateObjectCell } from 'mol-state/object';
 import { shallowEqual } from 'mol-util/object';
 import { shallowEqual } from 'mol-util/object';
 
 
 export { TransientTree }
 export { TransientTree }
 
 
 class TransientTree implements StateTree {
 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 changedNodes = false;
     private changedChildren = false;
     private changedChildren = false;
     private changedStates = 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() {
     private get childMutations() {
         if (this._childMutations) return this._childMutations;
         if (this._childMutations) return this._childMutations;
@@ -48,36 +47,32 @@ class TransientTree implements StateTree {
         this.children = this.children.asMutable();
         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() {
     asTransient() {
         return this.asImmutable().asTransient();
         return this.asImmutable().asTransient();
     }
     }
 
 
-    private addChild(parent: Transform.Ref, child: Transform.Ref) {
+    private addChild(parent: StateTransform.Ref, child: StateTransform.Ref) {
         this.changeChildren();
         this.changeChildren();
 
 
         if (this.childMutations.has(parent)) {
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.add(child);
             this.childMutations.get(parent)!.add(child);
         } else {
         } 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);
             set.add(child);
             this.children.set(parent, set);
             this.children.set(parent, set);
             this.childMutations.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();
         this.changeChildren();
 
 
         if (this.childMutations.has(parent)) {
         if (this.childMutations.has(parent)) {
             this.childMutations.get(parent)!.remove(child);
             this.childMutations.get(parent)!.remove(child);
         } else {
         } 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);
             set.remove(child);
             this.children.set(parent, set);
             this.children.set(parent, set);
             this.childMutations.set(parent, set);
             this.childMutations.set(parent, set);
@@ -85,17 +80,35 @@ class TransientTree implements StateTree {
     }
     }
 
 
     private clearRoot() {
     private clearRoot() {
-        const parent = Transform.RootRef;
+        const parent = StateTransform.RootRef;
         if (this.children.get(parent).size === 0) return;
         if (this.children.get(parent).size === 0) return;
 
 
         this.changeChildren();
         this.changeChildren();
 
 
-        const set = OrderedSet<Transform.Ref>();
+        const set = OrderedSet<StateTransform.Ref>();
         this.children.set(parent, set);
         this.children.set(parent, set);
         this.childMutations.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;
         const ref = transform.ref;
 
 
         if (this.transforms.has(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 */
     /** 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);
         ensurePresent(this.transforms, ref);
 
 
         const transform = this.transforms.get(ref)!;
         const transform = this.transforms.get(ref)!;
@@ -148,11 +161,11 @@ class TransientTree implements StateTree {
             this.transforms = this.transforms.asMutable();
             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;
         return true;
     }
     }
 
 
-    updateCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) {
+    updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) {
         ensurePresent(this.transforms, ref);
         ensurePresent(this.transforms, ref);
 
 
         const old = this.cellStates.get(ref);
         const old = this.cellStates.get(ref);
@@ -164,12 +177,12 @@ class TransientTree implements StateTree {
         return true;
         return true;
     }
     }
 
 
-    remove(ref: Transform.Ref): Transform[] {
+    remove(ref: StateTransform.Ref): StateTransform[] {
         const node = this.transforms.get(ref);
         const node = this.transforms.get(ref);
         if (!node) return [];
         if (!node) return [];
 
 
         const st = StateTree.subtreePostOrder(this, node);
         const st = StateTree.subtreePostOrder(this, node);
-        if (ref === Transform.RootRef) {
+        if (ref === StateTransform.RootRef) {
             st.pop();
             st.pop();
             if (st.length === 0) return st;
             if (st.length === 0) return st;
             this.clearRoot();
             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.`);
     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.`);
     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)) {
     if (!nodes.has(ref)) {
         throw new Error(`Node '${ref}' is not present in the tree.`);
         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 LocationColor = (location: Location, isSecondary: boolean) => Color
 
 
-export type ColorThemeProps = { [k: string]: any }
-
 export { ColorTheme }
 export { ColorTheme }
 interface ColorTheme<P extends PD.Params> {
 interface ColorTheme<P extends PD.Params> {
     readonly factory: ColorTheme.Factory<P>
     readonly factory: ColorTheme.Factory<P>
@@ -75,4 +73,5 @@ export const BuiltInColorThemes = {
     'shape-group': ShapeGroupColorThemeProvider,
     'shape-group': ShapeGroupColorThemeProvider,
     'unit-index': UnitIndexColorThemeProvider,
     'unit-index': UnitIndexColorThemeProvider,
     'uniform': UniformColorThemeProvider,
     'uniform': UniformColorThemeProvider,
-}
+}
+export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes

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

@@ -43,4 +43,5 @@ export const BuiltInSizeThemes = {
     'physical': PhysicalSizeThemeProvider,
     'physical': PhysicalSizeThemeProvider,
     'shape-group': ShapeGroupSizeThemeProvider,
     'shape-group': ShapeGroupSizeThemeProvider,
     'uniform': UniformSizeThemeProvider
     '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 * from './value-cell'
 export { BitFlags, StringBuilder, UUID, Mask }
 export { BitFlags, StringBuilder, UUID, Mask }
 
 
+export const noop = function () { };
+
 export function round(n: number, d: number) {
 export function round(n: number, d: number) {
     let f = Math.pow(10, d)
     let f = Math.pow(10, d)
     return Math.round(f * n) / f
     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 * as UTF8 from 'mol-io/common/utf8'
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
 
 
 export type Bool = { kind: 'bool' }
 export type Bool = { kind: 'bool' }
 export type Int = { kind: 'int' }
 export type Int = { kind: 'int' }
@@ -105,7 +106,7 @@ export function encode(element: Element, src: any): Buffer {
     return write(element, src);
     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) {
     switch (e.kind) {
         case 'bool': target.value = !!buffer.readInt8(offset); offset += 1; break;
         case 'bool': target.value = !!buffer.readInt8(offset); offset += 1; break;
         case 'int': target.value = buffer.readInt32LE(offset); offset += 4; 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;
     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 };
     const target = { value: void 0 as any };
     decodeElement(element, buffer, offset! | 0, target);
     decodeElement(element, buffer, offset! | 0, target);
     return target.value as T;
     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>
  * @author David Sehnal <david.sehnal@gmail.com>
  */
  */
 
 
-import * as File from './file'
 import * as Schema from './binary-schema'
 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 {
 export interface Spacegroup {
     number: number,
     number: number,
@@ -62,7 +53,7 @@ export interface Header {
     channels: string[],
     channels: string[],
 
 
     /** Determines the data type of the values */
     /** Determines the data type of the values */
-    valueType: ValueType,
+    valueType: TypedArrayValueType,
 
 
     /** The value are stored in blockSize^3 cubes */
     /** The value are stored in blockSize^3 cubes */
     blockSize: number,
     blockSize: number,
@@ -102,31 +93,16 @@ namespace _schema {
 
 
 const headerSchema = _schema.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) {
 export function encodeHeader(header: Header) {
     return Schema.encode(headerSchema, 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);
     const headerSize = buffer.readInt32LE(0);
 
 
     if (headerSize > buffer.byteLength - 4) {
     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);
     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 fs from 'fs'
 import * as path from 'path'
 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) {
 export async function openRead(filename: string) {
     return new Promise<number>((res, rej) => {
     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 {
 function makeDir(path: string, root?: string): boolean {
     let dirs = path.split(/\/|\\/g),
     let dirs = path.split(/\/|\\/g),
         dir = dirs.shift();
         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);
     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 pack from './pack/main'
 import VERSION from './pack/version'
 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,
     isPeriodic: false,
     outputFilename: '',
     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() {
 function printHelp() {
     let help = [
     let help = [
         `VolumeServer Packer ${VERSION}, (c) 2016 - now, David Sehnal`,
         `VolumeServer Packer ${VERSION}, (c) 2016 - now, David Sehnal`,
@@ -50,7 +67,10 @@ function parseInput() {
     for (let i = 2; i < process.argv.length; i++) {
     for (let i = 2; i < process.argv.length; i++) {
         switch (process.argv[i].toLowerCase()) {
         switch (process.argv[i].toLowerCase()) {
             case '-blocksize':
             case '-blocksize':
-                config.blockSize = +process.argv[++i];
+                config.blockSizeInMB = +process.argv[++i];
+                break;
+            case '-format':
+                config.format = getFormat(process.argv[++i]);
                 break;
                 break;
             case '-xray':
             case '-xray':
                 input = true;
                 input = true;
@@ -82,5 +102,5 @@ function parseInput() {
 }
 }
 
 
 if (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>
  * @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 * 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';
 const FORMAT_VERSION = '1.0.0';
 
 
@@ -23,16 +26,16 @@ export interface ValuesInfo {
 }
 }
 
 
 export interface BlockBuffer {
 export interface BlockBuffer {
-    values: DataFormat.ValueArray[],
-    buffers: Buffer[],
+    values: TypedArrayValueArray[],
+    buffers: SimpleBuffer[],
     slicesWritten: number
     slicesWritten: number
 }
 }
 
 
 export interface DownsamplingBuffer {
 export interface DownsamplingBuffer {
     /** dimensions (sampleCount[1], sampleCount[0] / 2, 1), axis order (K, H, L) */
     /** 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),  */
     /** "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,
     slicesWritten: number,
     startSliceIndex: number
     startSliceIndex: number
@@ -68,18 +71,18 @@ export interface Kernel {
 
 
 export interface Context {
 export interface Context {
     /** Output file handle  */
     /** Output file handle  */
-    file: number,
+    file: FileHandle,
 
 
     /** Periodic are x-ray density files that cover the entire grid and have [0,0,0] origin */
     /** Periodic are x-ray density files that cover the entire grid and have [0,0,0] origin */
     isPeriodic: boolean,
     isPeriodic: boolean,
 
 
-    channels: CCP4.Data[],
-    valueType: DataFormat.ValueType,
+    channels: Format.Context[],
+    valueType: TypedArrayValueType,
     blockSize: number,
     blockSize: number,
     /** Able to store channels.length * blockSize^3 values. */
     /** 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  */
     /** All values are stored in little endian format which might not be the native endian of the system  */
-    litteEndianCubeBuffer: Buffer,
+    litteEndianCubeBuffer: SimpleBuffer,
 
 
     kernel: Kernel,
     kernel: Kernel,
     sampling: Sampling[],
     sampling: Sampling[],
@@ -90,7 +93,7 @@ export interface Context {
 }
 }
 
 
 export function createHeader(ctx: Context): DataFormat.Header {
 export function createHeader(ctx: Context): DataFormat.Header {
-    const header = ctx.channels[0].header;
+    const header = ctx.channels[0].data.header;
     const grid = header.grid;
     const grid = header.grid;
 
 
     function normalize(data: number[]) {
     function normalize(data: number[]) {
@@ -99,13 +102,13 @@ export function createHeader(ctx: Context): DataFormat.Header {
 
 
     return {
     return {
         formatVersion: FORMAT_VERSION,
         formatVersion: FORMAT_VERSION,
-        valueType: CCP4.getValueType(header),
+        valueType: header.valueType,
         blockSize: ctx.blockSize,
         blockSize: ctx.blockSize,
         axisOrder: header.axisOrder,
         axisOrder: header.axisOrder,
         origin: normalize(header.origin),
         origin: normalize(header.origin),
         dimensions: normalize(header.extent),
         dimensions: normalize(header.extent),
         spacegroup: { number: header.spacegroupNumber, size: header.cellSize, angles: header.cellAngles, isPeriodic: ctx.isPeriodic },
         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 => {
         sampling: ctx.sampling.map(s => {
             const N = s.sampleCount[0] * s.sampleCount[1] * s.sampleCount[2];
             const N = s.sampleCount[0] * s.sampleCount[1] * s.sampleCount[2];
             const valuesInfo = [];
             const valuesInfo = [];

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

@@ -7,10 +7,10 @@
  */
  */
 
 
 import * as Data from './data-model'
 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.
  * higher rate downsampling.
  */
  */
 export function downsampleLayer(ctx: Data.Context) {
 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.
  * be some data left to be processed for the higher rate samplings.
  */
  */
 export function finalize(ctx: Data.Context) {
 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
  * 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.
  * 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:
  * The reason there are two copies of almost the same code is performance:
  * Both functions use a different memory layout to improve cache coherency
  * Both functions use a different memory layout to improve cache coherency
  *  - downsampleU uses the H axis as the fastest moving one
  *  - 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]);
     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
  * flipping the 1st and 2nd axis in the process to optimize cache coherency for downsampleUV call
  * (i.e. use (K, H, L) axis order).
  * (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 target = buffer.downsampleH;
     const sizeH = srcDims[0], sizeK = srcDims[1], srcBaseOffset = srcLOffset * sizeH * sizeK;
     const sizeH = srcDims[0], sizeK = srcDims[1], srcBaseOffset = srcLOffset * sizeH * sizeK;
     const targetH = Math.floor((sizeH + 1) / 2);
     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.
  * in the (L, H, K) axis order.
  */
  */
 function downsampleHK(kernel: Data.Kernel, dimsX: number[], buffer: Data.DownsamplingBuffer) {
 function downsampleHK(kernel: Data.Kernel, dimsX: number[], buffer: Data.DownsamplingBuffer) {

部分文件因为文件数量过多而无法显示