Browse Source

Merge branch 'master' into plugin

David Sehnal 5 years ago
parent
commit
64c51f0d94
100 changed files with 7018 additions and 2543 deletions
  1. 65 0
      .eslintrc.json
  2. 1 1
      .vscode/extensions.json
  3. 3 0
      .vscode/settings.json
  4. 2 1
      README.md
  5. 0 22
      data/rcsb-graphql/codegen.js
  6. 0 6
      data/rcsb-graphql/codegen.json
  7. 12 0
      data/rcsb-graphql/codegen.yml
  8. 0 14
      data/rcsb-graphql/loader.js
  9. 4223 1147
      package-lock.json
  10. 38 27
      package.json
  11. 4 4
      src/apps/basic-wrapper/coloring.ts
  12. 5 5
      src/apps/basic-wrapper/index.ts
  13. 10 10
      src/apps/basic-wrapper/superposition.ts
  14. 2 2
      src/apps/chem-comp-bond/create-table.ts
  15. 2 2
      src/apps/structure-info/volume.ts
  16. 3 1
      src/apps/viewer/index.ts
  17. 9 7
      src/examples/proteopedia-wrapper/annotation.ts
  18. 11 11
      src/examples/proteopedia-wrapper/index.ts
  19. 22 15
      src/mol-canvas3d/camera.ts
  20. 3 1
      src/mol-canvas3d/camera/transition.ts
  21. 78 30
      src/mol-canvas3d/canvas3d.ts
  22. 3 3
      src/mol-canvas3d/controls/trackball.ts
  23. 11 7
      src/mol-canvas3d/passes/draw.ts
  24. 9 7
      src/mol-canvas3d/passes/multi-sample.ts
  25. 6 4
      src/mol-canvas3d/passes/pick.ts
  26. 3 3
      src/mol-canvas3d/passes/postprocessing.ts
  27. 15 9
      src/mol-data/db/column.ts
  28. 2 2
      src/mol-data/generic/linked-list.ts
  29. 2 2
      src/mol-data/util/_spec/interval-iterator.spec.ts
  30. 67 27
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  31. 25 28
      src/mol-geo/geometry/geometry.ts
  32. 2 11
      src/mol-geo/geometry/lines/lines-builder.ts
  33. 81 32
      src/mol-geo/geometry/lines/lines.ts
  34. 90 0
      src/mol-geo/geometry/mesh/laplacian-smoothing.ts
  35. 2 12
      src/mol-geo/geometry/mesh/mesh-builder.ts
  36. 88 211
      src/mol-geo/geometry/mesh/mesh.ts
  37. 2 8
      src/mol-geo/geometry/points/points-builder.ts
  38. 70 20
      src/mol-geo/geometry/points/points.ts
  39. 2 10
      src/mol-geo/geometry/spheres/spheres-builder.ts
  40. 72 18
      src/mol-geo/geometry/spheres/spheres.ts
  41. 5 5
      src/mol-geo/geometry/text/font-atlas.ts
  42. 2 13
      src/mol-geo/geometry/text/text-builder.ts
  43. 81 21
      src/mol-geo/geometry/text/text.ts
  44. 16 15
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  45. 14 17
      src/mol-geo/primitive/box.ts
  46. 1 1
      src/mol-geo/primitive/cage.ts
  47. 16 16
      src/mol-geo/primitive/dodecahedron.ts
  48. 2 2
      src/mol-geo/primitive/icosahedron.ts
  49. 8 9
      src/mol-geo/primitive/polyhedron.ts
  50. 3 7
      src/mol-geo/primitive/tetrahedron.ts
  51. 99 70
      src/mol-geo/util.ts
  52. 4 14
      src/mol-geo/util/marching-cubes/builder.ts
  53. 20 22
      src/mol-gl/_spec/renderer.spec.ts
  54. 9 9
      src/mol-gl/compute/histogram-pyramid/reduction.ts
  55. 4 7
      src/mol-gl/compute/histogram-pyramid/sum.ts
  56. 6 8
      src/mol-gl/compute/marching-cubes/active-voxels.ts
  57. 12 15
      src/mol-gl/compute/marching-cubes/isosurface.ts
  58. 6 6
      src/mol-gl/compute/util.ts
  59. 29 43
      src/mol-gl/render-object.ts
  60. 1 1
      src/mol-gl/renderable/mesh.ts
  61. 11 11
      src/mol-gl/renderable/schema.ts
  62. 12 10
      src/mol-gl/renderer.ts
  63. 6 2
      src/mol-gl/shader-code.ts
  64. 1 1
      src/mol-gl/shader/chunks/common.glsl.ts
  65. 34 32
      src/mol-gl/webgl/buffer.ts
  66. 72 41
      src/mol-gl/webgl/context.ts
  67. 14 19
      src/mol-gl/webgl/framebuffer.ts
  68. 47 49
      src/mol-gl/webgl/program.ts
  69. 25 42
      src/mol-gl/webgl/render-item.ts
  70. 26 22
      src/mol-gl/webgl/render-target.ts
  71. 36 21
      src/mol-gl/webgl/renderbuffer.ts
  72. 155 0
      src/mol-gl/webgl/resources.ts
  73. 14 15
      src/mol-gl/webgl/shader.ts
  74. 35 13
      src/mol-gl/webgl/state.ts
  75. 120 81
      src/mol-gl/webgl/texture.ts
  76. 55 22
      src/mol-gl/webgl/vertex-array.ts
  77. 14 1
      src/mol-io/common/binary.ts
  78. 12 2
      src/mol-io/common/utf8.ts
  79. 25 7
      src/mol-io/reader/_spec/ccp4.spec.ts
  80. 17 2
      src/mol-io/reader/_spec/common.spec.ts
  81. 74 0
      src/mol-io/reader/_spec/dcd.spec.ts
  82. 0 1
      src/mol-io/reader/_spec/ply.spec.ts
  83. 110 0
      src/mol-io/reader/_spec/psf.spec.ts
  84. 6 5
      src/mol-io/reader/cif/data-model.ts
  85. 5 5
      src/mol-io/reader/common/text/column/token.ts
  86. 16 4
      src/mol-io/reader/common/text/number-parser.ts
  87. 2 2
      src/mol-io/reader/common/text/tokenizer.ts
  88. 215 0
      src/mol-io/reader/dcd/parser.ts
  89. 12 15
      src/mol-io/reader/ply/parser.ts
  90. 213 0
      src/mol-io/reader/psf/parser.ts
  91. 47 9
      src/mol-math/geometry/centroid-helper.ts
  92. 14 17
      src/mol-math/geometry/gaussian-density/gpu.ts
  93. 12 2
      src/mol-math/geometry/lookup3d/common.ts
  94. 13 13
      src/mol-math/geometry/lookup3d/grid.ts
  95. 4 2
      src/mol-math/geometry/primitives/sphere3d.ts
  96. 23 0
      src/mol-math/geometry/spacegroup/cell.ts
  97. 43 42
      src/mol-math/graph/int-adjacency-graph.ts
  98. 58 8
      src/mol-math/graph/inter-unit-graph.ts
  99. 3 3
      src/mol-math/linear-algebra/3d.ts
  100. 34 1
      src/mol-math/linear-algebra/3d/mat4.ts

+ 65 - 0
.eslintrc.json

@@ -0,0 +1,65 @@
+{
+    "env": {
+        "browser": true,
+        "node": true
+    },
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "project": "tsconfig.json",
+        "sourceType": "module"
+    },
+    "plugins": [
+        "@typescript-eslint"
+    ],
+    "rules": {
+        "@typescript-eslint/ban-types": "warn",
+        "@typescript-eslint/class-name-casing": "off",
+        "@typescript-eslint/indent": [
+            "warn",
+            4
+        ],
+        "@typescript-eslint/member-delimiter-style": [
+            "off",
+            {
+                "multiline": {
+                    "delimiter": "none",
+                    "requireLast": true
+                },
+                "singleline": {
+                    "delimiter": "semi",
+                    "requireLast": false
+                }
+            }
+        ],
+        "@typescript-eslint/prefer-namespace-keyword": "warn",
+        "@typescript-eslint/quotes": [
+            "warn",
+            "single",
+            { 
+                "avoidEscape": true,
+                "allowTemplateLiterals": true
+            }
+        ],
+        "@typescript-eslint/semi": [
+            "off",
+            null
+        ],
+        "@typescript-eslint/type-annotation-spacing": "warn",
+        "arrow-parens": [
+            "off",
+            "as-needed"
+        ],
+        "comma-dangle": "off",
+        "eqeqeq": [
+            "warn",
+            "smart"
+        ],
+        "import/order": "off",
+        "no-eval": "warn",
+        "no-new-wrappers": "warn",
+        "no-trailing-spaces": "warn",
+        "no-unsafe-finally": "warn",
+        "no-var": "warn",
+        "spaced-comment": "warn"
+    }
+}

+ 1 - 1
.vscode/extensions.json

@@ -4,8 +4,8 @@
 
 	// List of extensions which should be recommended for users of this workspace.
 	"recommendations": [
+		"dbaeumer.vscode-eslint",
 		"firsttris.vscode-jest-runner",
-		"ms-vscode.vscode-typescript-tslint-plugin",
 		"msjsdiag.debugger-for-chrome",
 		"slevesque.shader",
 		"stpn.vscode-graphql",

+ 3 - 0
.vscode/settings.json

@@ -6,4 +6,7 @@
         "*.vert.ts": "glsl",
         "*.gql.ts": "graphql"
     },
+    "eslint.options": {
+        "ignorePattern": ["webpack.config.js", "scripts/*"],
+    }
 }

+ 2 - 1
README.md

@@ -38,6 +38,7 @@ Moreover, the project contains the imlementation of `servers`, including
 
 - `servers/model` A tool for accessing coordinate and annotation data of molecular structures.
 - `servers/volume` A tool for accessing volumetric experimental data related to molecular structures.
+- `servers/plugin-state` A basic server to store Mol* Plugin states.
 
 The project also contains performance tests (`perf-tests`), `examples`, and basic proof of concept `apps` (CIF to BinaryCIF converter and JSON domain annotation to CIF converter).
 
@@ -93,7 +94,7 @@ Install CIFTools `npm install ciftools -g`
 
 **GraphQL schemas**
 
-    node data/rcsb-graphql/codegen.js
+    ./node_modules/.bin/graphql-codegen -c ./data/rcsb-graphql/codegen.yml
 
 ### Other scripts
 **Create chem comp bond table**

+ 0 - 22
data/rcsb-graphql/codegen.js

@@ -1,22 +0,0 @@
-const { generate } = require('graphql-code-generator')
-const path = require('path')
-
-const basePath = path.join(__dirname, '..', '..', 'src', 'mol-model-props', 'rcsb', 'graphql')
-
-generate({
-    schema: 'http://rest-staging.rcsb.org/graphql',
-    documents: {
-        [path.join(basePath, 'symmetry.gql.ts')]: {
-            loader: path.join(__dirname, 'loader.js')
-        },
-    },
-    generates: {
-        [path.join(basePath, 'types.ts')]: {
-            plugins: ['time', 'typescript-common', 'typescript-client']
-        }
-    },
-    overwrite: true,
-    config: path.join(__dirname, 'codegen.json')
-}, true).then(
-    () => console.log('done')
-).catch(e => console.error(e))

+ 0 - 6
data/rcsb-graphql/codegen.json

@@ -1,6 +0,0 @@
-{
-    "flattenTypes": true,
-    "generatorConfig": {
-        "immutableTypes": true
-    }
-}

+ 12 - 0
data/rcsb-graphql/codegen.yml

@@ -0,0 +1,12 @@
+schema: http://data-beta.rcsb.org/graphql
+documents: './src/mol-model-props/rcsb/graphql/symmetry.gql.ts'
+generates:
+  './src/mol-model-props/rcsb/graphql/types.d.ts':
+    plugins:
+      - add: '/* eslint-disable */'
+      - time
+      - typescript
+      - typescript-operations
+    config:
+      immutableTypes: true
+      skipTypename: true

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

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

File diff suppressed because it is too large
+ 4223 - 1147
package-lock.json


+ 38 - 27
package.json

@@ -11,7 +11,7 @@
     "url": "https://github.com/molstar/molstar/issues"
   },
   "scripts": {
-    "lint": "tslint src/**/*.ts",
+    "lint": "eslint src/**/*.ts",
     "test": "npm run lint && jest",
     "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
     "build-tsc": "tsc --incremental",
@@ -24,7 +24,8 @@
     "serve": "http-server -p 1338",
     "model-server": "node lib/servers/model/server.js",
     "model-server-watch": "nodemon --watch lib lib/servers/model/server.js",
-    "volume-server": "node lib/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort  1336",
+    "volume-server": "node lib/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
+    "plugin-state": "node lib/servers/plugin-state/index.js",
     "preversion": "npm run test",
     "postversion": "git push && git push --tags",
     "prepublishOnly": "npm run test && npm run build"
@@ -63,56 +64,66 @@
   ],
   "license": "MIT",
   "devDependencies": {
+    "@graphql-codegen/add": "^1.11.2",
+    "@graphql-codegen/cli": "^1.11.2",
+    "@graphql-codegen/time": "^1.11.2",
+    "@graphql-codegen/typescript": "^1.11.2",
+    "@graphql-codegen/typescript-graphql-files-modules": "^1.11.2",
+    "@graphql-codegen/typescript-graphql-request": "^1.11.2",
+    "@graphql-codegen/typescript-operations": "^1.11.2",
+    "@types/cors": "^2.8.6",
+    "@typescript-eslint/eslint-plugin": "^2.17.0",
+    "@typescript-eslint/eslint-plugin-tslint": "^2.17.0",
+    "@typescript-eslint/parser": "^2.17.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.2.0",
-    "concurrently": "^5.0.1",
-    "cpx": "^1.5.0",
-    "css-loader": "^3.3.2",
+    "concurrently": "^5.0.2",
+    "cpx2": "^2.0.0",
+    "css-loader": "^3.4.2",
+    "eslint": "^6.8.0",
     "extra-watch-webpack-plugin": "^1.0.3",
     "file-loader": "^5.0.2",
     "fs-extra": "^8.1.0",
-    "graphql-code-generator": "^0.18.2",
-    "graphql-codegen-time": "^0.18.2",
-    "graphql-codegen-typescript-template": "^0.18.2",
-    "http-server": "^0.12.0",
-    "jest": "^24.9.0",
+    "http-server": "^0.12.1",
+    "jest": "^25.1.0",
     "jest-raw-loader": "^1.0.1",
-    "mini-css-extract-plugin": "^0.8.0",
-    "node-sass": "^4.13.0",
+    "mini-css-extract-plugin": "^0.9.0",
+    "node-sass": "^4.13.1",
+    "pascal-case": "^3.1.1",
     "raw-loader": "^4.0.0",
     "resolve-url-loader": "^3.1.1",
-    "sass-loader": "^8.0.0",
-    "simple-git": "^1.129.0",
-    "style-loader": "^1.0.1",
-    "ts-jest": "^24.2.0",
-    "tslint": "^5.20.1",
-    "typescript": "^3.7.3",
-    "webpack": "^4.41.3",
+    "sass-loader": "^8.0.2",
+    "simple-git": "^1.130.0",
+    "style-loader": "^1.1.3",
+    "ts-jest": "^25.0.0",
+    "typescript": "^3.7.5",
+    "webpack": "^4.41.5",
     "webpack-cli": "^3.3.10"
   },
   "dependencies": {
-    "@types/argparse": "^1.0.36",
+    "@types/argparse": "^1.0.38",
     "@types/benchmark": "^1.0.31",
     "@types/compression": "1.0.1",
     "@types/express": "^4.17.2",
-    "@types/jest": "^24.0.23",
-    "@types/node": "^12.12.18",
+    "@types/jest": "^24.9.0",
+    "@types/node": "^13.1.8",
     "@types/node-fetch": "^2.5.4",
-    "@types/react": "^16.9.16",
-    "@types/react-dom": "^16.9.4",
+    "@types/react": "^16.9.19",
+    "@types/react-dom": "^16.9.5",
     "@types/swagger-ui-dist": "3.0.5",
     "argparse": "^1.0.10",
     "body-parser": "^1.19.0",
     "compression": "^1.7.4",
+    "cors": "^2.8.5",
     "express": "^4.17.1",
     "graphql": "^14.5.8",
     "immutable": "^3.8.2",
     "node-fetch": "^2.6.0",
     "react": "^16.12.0",
     "react-dom": "^16.12.0",
-    "rxjs": "^6.5.3",
-    "swagger-ui-dist": "^3.24.3",
-    "util.promisify": "^1.0.0",
+    "rxjs": "^6.5.4",
+    "swagger-ui-dist": "^3.25.0",
+    "util.promisify": "^1.0.1",
     "xhr2": "^0.2.0"
   }
 }

+ 4 - 4
src/apps/basic-wrapper/coloring.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 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 { CustomElementProperty } from '../../mol-model-props/common/custom-element-property';
@@ -9,9 +10,8 @@ import { Model, ElementIndex } from '../../mol-model/structure';
 import { Color } from '../../mol-util/color';
 
 export const StripedResidues = CustomElementProperty.create<number>({
-    isStatic: true,
+    label: 'Residue Stripes',
     name: 'basic-wrapper-residue-striping',
-    display: 'Residue Stripes',
     getData(model: Model) {
         const map = new Map<ElementIndex, number>();
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
@@ -24,7 +24,7 @@ export const StripedResidues = CustomElementProperty.create<number>({
         getColor(e) { return e === 0 ? Color(0xff0000) : Color(0x0000ff) },
         defaultColor: Color(0x777777)
     },
-    format(e) {
+    getLabel(e) {
         return e === 0 ? 'Odd stripe' : 'Even stripe'
     }
 })

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

@@ -47,9 +47,9 @@ class BasicWrapper {
             }
         });
 
-        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.Descriptor.name, StripedResidues.colorTheme!);
-        this.plugin.lociLabels.addProvider(StripedResidues.labelProvider);
-        this.plugin.customModelProperties.register(StripedResidues.propertyProvider);
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.propertyProvider.descriptor.name, StripedResidues.colorThemeProvider!);
+        this.plugin.lociLabels.addProvider(StripedResidues.labelProvider!);
+        this.plugin.customModelProperties.register(StripedResidues.propertyProvider, true);
     }
 
     private download(b: StateBuilder.To<PSO.Root>, url: string) {
@@ -63,7 +63,7 @@ class BasicWrapper {
 
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.propertyProvider.descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 
@@ -141,7 +141,7 @@ class BasicWrapper {
 
             const visuals = state.selectQ(q => q.ofTransformer(StateTransforms.Representation.StructureRepresentation3D));
             const tree = state.build();
-            const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues };
+            const colorTheme = { name: StripedResidues.propertyProvider.descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.propertyProvider.descriptor.name).defaultValues };
 
             for (const v of visuals) {
                 tree.to(v).update(old => ({ ...old, colorTheme }));

+ 10 - 10
src/apps/basic-wrapper/superposition.ts

@@ -50,16 +50,16 @@ export function buildStaticSuperposition(ctx: PluginContext, src: SuperpositionT
 
 export const StaticSuperpositionTestData: SuperpositionTestInput = [
     { pdbId: '1aj5', auth_asym_id: 'A', matrix: Mat4.identity() },
-    { pdbId: '1df0', auth_asym_id: 'B', matrix: Mat4.ofRows(
-        [[0.406, 0.879, 0.248, -200.633],
-         [0.693, -0.473, 0.544, 73.403],
-         [0.596, -0.049, -0.802, -14.209],
-         [0, 0, 0, 1]] )},
-    { pdbId: '1dvi', auth_asym_id: 'A', matrix: Mat4.ofRows(
-        [[-0.053, -0.077, 0.996, -45.633],
-         [-0.312, 0.949, 0.057, -12.255],
-         [-0.949, -0.307, -0.074, 53.562],
-         [0, 0, 0, 1]] )}
+    { pdbId: '1df0', auth_asym_id: 'B', matrix: Mat4.ofRows([
+        [0.406, 0.879, 0.248, -200.633],
+        [0.693, -0.473, 0.544, 73.403],
+        [0.596, -0.049, -0.802, -14.209],
+        [0, 0, 0, 1]] )},
+    { pdbId: '1dvi', auth_asym_id: 'A', matrix: Mat4.ofRows([
+        [-0.053, -0.077, 0.996, -45.633],
+        [-0.312, 0.949, 0.057, -12.255],
+        [-0.949, -0.307, -0.074, 53.562],
+        [0, 0, 0, 1]] )}
 ];
 
 export async function dynamicSuperpositionTest(ctx: PluginContext, src: string[], comp_id: string) {

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

@@ -226,8 +226,8 @@ const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif'
 const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif'
 
 const parser = new argparse.ArgumentParser({
-  addHelp: true,
-  description: 'Create a cif file with one big table of all chem_comp_bond entries from the CCD and PVCD.'
+    addHelp: true,
+    description: 'Create a cif file with one big table of all chem_comp_bond entries from the CCD and PVCD.'
 });
 parser.addArgument('out', {
     help: 'Generated file output path.'

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

@@ -77,8 +77,8 @@ async function run(url: string, meshFilename: string) {
 }
 
 const parser = new argparse.ArgumentParser({
-addHelp: true,
-description: 'Info about VolumeData from mol-model module'
+    addHelp: true,
+    description: 'Info about VolumeData from mol-model module'
 });
 parser.addArgument([ '--emdb', '-e' ], {
     help: 'EMDB id, for example 8116',

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

@@ -5,6 +5,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import '../../mol-util/polyfill';
 import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
 import './index.html'
 import './favicon.ico'
@@ -42,7 +43,8 @@ function init() {
             controls: {
                 ...DefaultPluginSpec.layout && DefaultPluginSpec.layout.controls
             }
-        }
+        },
+        config: DefaultPluginSpec.config
     };
     const plugin = createPlugin(document.getElementById('app')!, spec);
     trySetSnapshot(plugin);

+ 9 - 7
src/examples/proteopedia-wrapper/annotation.ts

@@ -1,12 +1,14 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 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 { CustomElementProperty } from '../../mol-model-props/common/custom-element-property';
 import { Model, ElementIndex, ResidueIndex } from '../../mol-model/structure';
 import { Color } from '../../mol-util/color';
+import { CustomProperty } from '../../mol-model-props/common/custom-property';
 
 const EvolutionaryConservationPalette: Color[] = [
     [255, 255, 129], // insufficient
@@ -23,13 +25,13 @@ const EvolutionaryConservationPalette: Color[] = [
 const EvolutionaryConservationDefaultColor = Color(0x999999);
 
 export const EvolutionaryConservation = CustomElementProperty.create<number>({
-    isStatic: true,
     name: 'proteopedia-wrapper-evolutionary-conservation',
-    display: 'Evolutionary Conservation',
-    async getData(model: Model) {
+    label: 'Evolutionary Conservation',
+    type: 'static',
+    async getData(model: Model, ctx: CustomProperty.Context) {
         const id = model.entryId.toLowerCase();
-        const req = await fetch(`https://proteopedia.org/cgi-bin/cnsrf?${id}`);
-        const json = await req.json();
+        const url = `https://proteopedia.org/cgi-bin/cnsrf?${id}`
+        const json = await ctx.fetch({ url, type: 'json' }).runInContext(ctx.runtime)
         const annotations = (json && json.residueAnnotations) || [];
 
         const conservationMap = new Map<string, number>();
@@ -65,7 +67,7 @@ export const EvolutionaryConservation = CustomElementProperty.create<number>({
         },
         defaultColor: EvolutionaryConservationDefaultColor
     },
-    format(e) {
+    getLabel(e) {
         if (e === 10) return `Evolutionary Conservation: InsufficientData`;
         return e ? `Evolutionary Conservation: ${e}` : void 0;
     }

+ 11 - 11
src/examples/proteopedia-wrapper/index.ts

@@ -68,9 +68,9 @@ class MolStarProteopediaWrapper {
         const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []);
 
         this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add('proteopedia-custom', customColoring);
-        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!);
-        this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider);
-        this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider);
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.propertyProvider.descriptor.name, EvolutionaryConservation.colorThemeProvider!);
+        this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider!);
+        this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider, true);
     }
 
     get state() {
@@ -94,7 +94,7 @@ class MolStarProteopediaWrapper {
         const model = this.state.build().to(StateElements.Model);
 
         const s = model
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.propertyProvider.descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly });
 
         s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence });
@@ -160,9 +160,9 @@ class MolStarProteopediaWrapper {
                 root.delete(StateElements.WaterVisual);
             } else {
                 root.applyOrUpdate(StateElements.WaterVisual, StateTransforms.Representation.StructureRepresentation3D,
-                        StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
-                            (style.water && style.water.kind) || 'ball-and-stick',
-                            (style.water && style.water.coloring), structure, { alpha: 0.51 }));
+                    StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                        (style.water && style.water.kind) || 'ball-and-stick',
+                        (style.water && style.water.coloring), structure, { alpha: 0.51 }));
             }
         }
 
@@ -299,7 +299,7 @@ class MolStarProteopediaWrapper {
             // }
 
             const tree = state.build();
-            const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
+            const colorTheme = { name: EvolutionaryConservation.propertyProvider.descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.propertyProvider.descriptor.name).defaultValues };
 
             if (!params || !!params.sequence) {
                 tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
@@ -385,7 +385,8 @@ class MolStarProteopediaWrapper {
             // const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter);
             // Vec3.normalize(position, position);
             // Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius);
-            const snapshot = this.plugin.canvas3d!.camera.getFocus(sphere.center, Math.max(sphere.radius, 5));
+            const radius = Math.max(sphere.radius, 5)
+            const snapshot = this.plugin.canvas3d!.camera.getFocus(sphere.center, radius, radius);
             PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 });
         }
     }
@@ -417,8 +418,7 @@ class MolStarProteopediaWrapper {
         },
         download: async (url: string) => {
             try {
-                const data = await this.plugin.runTask(this.plugin.fetch({ url }));
-                const snapshot = JSON.parse(data);
+                const snapshot = await this.plugin.runTask(this.plugin.fetch({ url, type: 'json' }));
                 await this.plugin.state.setSnapshot(snapshot);
             } catch (e) {
                 console.log(e);

+ 22 - 15
src/mol-canvas3d/camera.ts

@@ -82,12 +82,12 @@ class Camera {
         return Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
     }
 
-    getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
+    getFocus(target: Vec3, radiusNear: number, radiusFar: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
         const fov = this.state.fov
         const { width, height } = this.viewport
         const aspect = width / height
         const aspectFactor = (height < width ? 1 : aspect)
-        const targetDistance = Math.abs((radius / aspectFactor) / Math.sin(fov / 2))
+        const targetDistance = Math.abs((radiusNear / aspectFactor) / Math.sin(fov / 2))
 
         Vec3.sub(this.deltaDirection, this.target, this.position)
         if (dir) Vec3.matchDirection(this.deltaDirection, dir, this.deltaDirection)
@@ -96,15 +96,18 @@ class Camera {
 
         const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state)
         state.target = Vec3.clone(target)
-        state.radius = radius
+        state.radiusNear = radiusNear
+        state.radiusFar = radiusFar
         state.position = Vec3.clone(this.newPosition)
         if (up) Vec3.matchDirection(state.up, up, state.up)
 
         return state
     }
 
-    focus(target: Vec3, radius: number, durationMs?: number, up?: Vec3, dir?: Vec3) {
-        if (radius > 0) this.setState(this.getFocus(target, radius, up, dir), durationMs);
+    focus(target: Vec3, radiusNear: number, radiusFar: number, durationMs?: number, up?: Vec3, dir?: Vec3) {
+        if (radiusNear > 0 && radiusFar > 0) {
+            this.setState(this.getFocus(target, radiusNear, radiusFar, up, dir), durationMs);
+        }
     }
 
     project(out: Vec4, point: Vec3) {
@@ -158,8 +161,10 @@ namespace Camera {
             up: Vec3.create(0, 1, 0),
             target: Vec3.create(0, 0, 0),
 
-            radius: 10,
+            radiusNear: 10,
+            radiusFar: 10,
             fog: 50,
+            clipFar: true
         };
     }
 
@@ -171,8 +176,10 @@ namespace Camera {
         up: Vec3
         target: Vec3
 
-        radius: number
+        radiusNear: number
+        radiusFar: number
         fog: number
+        clipFar: boolean
     }
 
     export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -185,8 +192,10 @@ namespace Camera {
         if (typeof source.up !== 'undefined') Vec3.copy(out.up, source.up);
         if (typeof source.target !== 'undefined') Vec3.copy(out.target, source.target);
 
-        if (typeof source.radius !== 'undefined') out.radius = source.radius;
+        if (typeof source.radiusNear !== 'undefined') out.radiusNear = source.radiusNear;
+        if (typeof source.radiusFar !== 'undefined') out.radiusFar = source.radiusFar;
         if (typeof source.fog !== 'undefined') out.fog = source.fog;
+        if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
 
         return out;
     }
@@ -253,17 +262,15 @@ function updatePers(camera: Camera) {
 }
 
 function updateClip(camera: Camera) {
-    const { radius, mode, fog } = camera.state
+    const { radiusNear, radiusFar, mode, fog, clipFar } = camera.state
 
     const cDist = Vec3.distance(camera.position, camera.target)
-    const bRadius = Math.max(1, radius)
-
-    let near = cDist - bRadius
-    let far = cDist + bRadius
+    let near = cDist - radiusNear
+    let far = cDist + (clipFar ? radiusNear : radiusFar)
 
     const fogNearFactor = -(50 - fog) / 50
-    let fogNear = cDist - (bRadius * fogNearFactor)
-    let fogFar = cDist + bRadius
+    let fogNear = cDist - (radiusNear * fogNearFactor)
+    let fogFar = far
 
     if (mode === 'perspective') {
         // set at least to 5 to avoid slow sphere impostor rendering

+ 3 - 1
src/mol-canvas3d/camera/transition.ts

@@ -79,7 +79,9 @@ namespace CameraTransitionManager {
         // Lerp target, position & radius
         Vec3.lerp(out.target, source.target, target.target, t);
         Vec3.lerp(out.position, source.position, target.position, t);
-        out.radius = lerp(source.radius, target.radius, t);
+        out.radiusNear = lerp(source.radiusNear, target.radiusNear, t);
+        // TODO take change of `clipFar` into account
+        out.radiusFar = lerp(source.radiusFar, target.radiusFar, t);
 
         // Lerp fov & fog
         out.fov = lerp(source.fov, target.fov, t);

+ 78 - 30
src/mol-canvas3d/canvas3d.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -26,17 +26,19 @@ import { SetUtils } from '../mol-util/set';
 import { Canvas3dInteractionHelper } from './helper/interaction-events';
 import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
 import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
-import { GLRenderingContext } from '../mol-gl/webgl/compat';
 import { PixelData } from '../mol-util/image';
 import { readTexture } from '../mol-gl/compute/util';
 import { DrawPass } from './passes/draw';
 import { PickPass } from './passes/pick';
 import { Task } from '../mol-task';
 import { ImagePass, ImageProps } from './passes/image';
+import { Sphere3D } from '../mol-math/geometry';
+import { isDebugMode } from '../mol-util/debug';
 
 export const Canvas3DParams = {
     cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
     cameraFog: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
+    cameraClipFar: PD.Boolean(true),
     cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
     transparentBackground: PD.Boolean(false),
 
@@ -73,6 +75,7 @@ interface Canvas3D {
     /** Focuses camera on scene's bounding sphere, centered and zoomed. */
     resetCamera: () => void
     readonly camera: Camera
+    readonly boundingSphere: Readonly<Sphere3D>
     downloadScreenshot: () => void
     getPixelData: (variant: GraphicsRenderVariant) => PixelData
     setProps: (props: Partial<Canvas3DProps>) => void
@@ -104,10 +107,45 @@ namespace Canvas3D {
         })
         if (gl === null) throw new Error('Could not create a WebGL rendering context')
         const input = InputObserver.fromElement(canvas)
-        return Canvas3D.create(gl, input, props, runTask)
+        const webgl = createContext(gl)
+
+        if (isDebugMode) {
+            const loseContextExt = gl.getExtension('WEBGL_lose_context')
+            if (loseContextExt) {
+                canvas.addEventListener('mousedown', e => {
+                    if (webgl.isContextLost) return
+                    if (!e.shiftKey || !e.ctrlKey || !e.altKey) return
+
+                    console.log('lose context')
+                    loseContextExt.loseContext()
+
+                    setTimeout(() => {
+                        if (!webgl.isContextLost) return
+                        console.log('restore context')
+                        loseContextExt.restoreContext()
+                    }, 1000)
+                }, false)
+            }
+        }
+
+        // https://www.khronos.org/webgl/wiki/HandlingContextLost
+
+        canvas.addEventListener('webglcontextlost', e => {
+            webgl.setContextLost()
+            e.preventDefault()
+            if (isDebugMode) console.log('context lost')
+        }, false)
+
+        canvas.addEventListener('webglcontextrestored', () => {
+            if (!webgl.isContextLost) return
+            webgl.handleContextRestored()
+            if (isDebugMode) console.log('context restored')
+        }, false)
+
+        return Canvas3D.create(webgl, input, props, runTask)
     }
 
-    export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
+    export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
         const p = { ...DefaultCanvas3DParams, ...props }
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
@@ -117,7 +155,7 @@ namespace Canvas3D {
         const startTime = now()
         const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
 
-        const webgl = createContext(gl)
+        const { gl, contextRestored } = webgl
 
         let width = gl.drawingBufferWidth
         let height = gl.drawingBufferHeight
@@ -127,7 +165,8 @@ namespace Canvas3D {
         const camera = new Camera({
             position: Vec3.create(0, 0, 100),
             mode: p.cameraMode,
-            fog: p.cameraFog
+            fog: p.cameraFog,
+            clipFar: p.cameraClipFar
         })
 
         const controls = TrackballControls.create(input, camera, p.trackball)
@@ -140,6 +179,11 @@ namespace Canvas3D {
         const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
         const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
 
+        const contextRestoredSub = contextRestored.subscribe(() => {
+            pickPass.pickDirty = true
+            draw(true)
+        })
+
         let drawPending = false
         let cameraResetRequested = false
 
@@ -175,8 +219,8 @@ namespace Canvas3D {
             }
         }
 
-        function render(variant: 'pick' | 'draw', force: boolean) {
-            if (scene.isCommiting) return false
+        function render(force: boolean) {
+            if (scene.isCommiting || webgl.isContextLost) return false
 
             let didRender = false
             controls.update(currentTime)
@@ -185,21 +229,14 @@ namespace Canvas3D {
             multiSample.update(force || cameraChanged, currentTime)
 
             if (force || cameraChanged || multiSample.enabled) {
-                switch (variant) {
-                    case 'pick':
-                        pickPass.render()
-                        break;
-                    case 'draw':
-                        renderer.setViewport(0, 0, width, height)
-                        if (multiSample.enabled) {
-                            multiSample.render(true, p.transparentBackground)
-                        } else {
-                            drawPass.render(!postprocessing.enabled, p.transparentBackground)
-                            if (postprocessing.enabled) postprocessing.render(true)
-                        }
-                        pickPass.pickDirty = true
-                        break;
+                renderer.setViewport(0, 0, width, height)
+                if (multiSample.enabled) {
+                    multiSample.render(true, p.transparentBackground)
+                } else {
+                    drawPass.render(!postprocessing.enabled, p.transparentBackground)
+                    if (postprocessing.enabled) postprocessing.render(true)
                 }
+                pickPass.pickDirty = true
                 didRender = true
             }
 
@@ -210,7 +247,7 @@ namespace Canvas3D {
         let currentTime = 0;
 
         function draw(force?: boolean) {
-            if (render('draw', !!force || forceNextDraw)) {
+            if (render(!!force || forceNextDraw)) {
                 didDraw.next(now() - startTime as now.Timestamp)
             }
             forceNextDraw = false;
@@ -227,20 +264,23 @@ namespace Canvas3D {
             currentTime = now();
             camera.transition.tick(currentTime);
             draw(false);
-            if (!camera.transition.inTransition) interactionHelper.tick(currentTime);
+            if (!camera.transition.inTransition && !webgl.isContextLost) {
+                interactionHelper.tick(currentTime);
+            }
             requestAnimationFrame(animate)
         }
 
         function identify(x: number, y: number): PickingId | undefined {
-            return pickPass.identify(x, y)
+            return webgl.isContextLost ? undefined : pickPass.identify(x, y)
         }
 
-        function commit(renderObjects?: readonly GraphicsRenderObject[]) {
+        async function commit(renderObjects?: readonly GraphicsRenderObject[]) {
             scene.update(renderObjects, false)
 
             return runTask(scene.commit()).then(() => {
                 if (cameraResetRequested && !scene.isCommiting) {
-                    camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
+                    const { center, radius } = scene.boundingSphere
+                    camera.focus(center, radius, radius)
                     cameraResetRequested = false
                 }
                 if (debugHelper.isEnabled) debugHelper.update()
@@ -256,8 +296,8 @@ namespace Canvas3D {
 
             if (oldRO) {
                 if (!SetUtils.areEqual(newRO, oldRO)) {
-                    for (const o of Array.from(newRO)) { if (!oldRO.has(o)) scene.add(o) }
-                    for (const o of Array.from(oldRO)) { if (!newRO.has(o)) scene.remove(o) }
+                    newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o) })
+                    oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o) })
                 }
             } else {
                 repr.renderObjects.forEach(o => scene.add(o))
@@ -320,11 +360,13 @@ namespace Canvas3D {
                 if (scene.isCommiting) {
                     cameraResetRequested = true
                 } else {
-                    camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, p.cameraResetDurationMs)
+                    const { center, radius } = scene.boundingSphere
+                    camera.focus(center, radius, radius, p.cameraResetDurationMs)
                     requestDraw(true);
                 }
             },
             camera,
+            boundingSphere: scene.boundingSphere,
             downloadScreenshot: () => {
                 // TODO
             },
@@ -346,6 +388,9 @@ namespace Canvas3D {
                 if (props.cameraFog !== undefined && props.cameraFog !== camera.state.fog) {
                     camera.setState({ fog: props.cameraFog })
                 }
+                if (props.cameraClipFar !== undefined && props.cameraClipFar !== camera.state.clipFar) {
+                    camera.setState({ clipFar: props.cameraClipFar })
+                }
                 if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
                 if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground
 
@@ -364,6 +409,7 @@ namespace Canvas3D {
                 return {
                     cameraMode: camera.state.mode,
                     cameraFog: camera.state.fog,
+                    cameraClipFar: camera.state.clipFar,
                     cameraResetDurationMs: p.cameraResetDurationMs,
                     transparentBackground: p.transparentBackground,
 
@@ -384,6 +430,8 @@ namespace Canvas3D {
                 return interactionHelper.events
             },
             dispose: () => {
+                contextRestoredSub.unsubscribe()
+
                 scene.clear()
                 debugHelper.clear()
                 input.dispose()

+ 3 - 3
src/mol-canvas3d/controls/trackball.ts

@@ -208,8 +208,8 @@ namespace TrackballControls {
         function focusCamera() {
             const factor = (_focusEnd[1] - _focusStart[1]) * p.zoomSpeed
             if (factor !== 0.0) {
-                const radius = Math.max(1, camera.state.radius + 10 * factor)
-                camera.setState({ radius })
+                const radiusNear = Math.max(1, camera.state.radiusNear + 10 * factor)
+                camera.setState({ radiusNear })
             }
 
             if (p.staticMoving) {
@@ -343,7 +343,7 @@ namespace TrackballControls {
             if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2)
             if (dragFocusZoom) {
                 const dist = Vec3.distance(camera.state.position, camera.state.target);
-                camera.setState({ radius: dist / 5 })
+                camera.setState({ radiusNear: dist / 5 })
             }
             if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2)
         }

+ 11 - 7
src/mol-canvas3d/passes/draw.ts

@@ -5,11 +5,11 @@
  */
 
 import { WebGLContext } from '../../mol-gl/webgl/context';
-import { createRenderTarget, RenderTarget } from '../../mol-gl/webgl/render-target';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
-import { createTexture, Texture } from '../../mol-gl/webgl/texture';
+import { Texture } from '../../mol-gl/webgl/texture';
 import { Camera } from '../camera';
 
 export class DrawPass {
@@ -20,13 +20,13 @@ export class DrawPass {
     private depthTarget: RenderTarget | null
 
     constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper) {
-        const { gl, extensions } = webgl
+        const { gl, extensions, resources } = webgl
         const width = gl.drawingBufferWidth
         const height = gl.drawingBufferHeight
-        this.colorTarget = createRenderTarget(webgl, width, height)
+        this.colorTarget = webgl.createRenderTarget(width, height)
         this.packedDepth = !extensions.depthTexture
-        this.depthTarget = this.packedDepth ? createRenderTarget(webgl, width, height) : null
-        this.depthTexture = this.depthTarget ? this.depthTarget.texture : createTexture(webgl, 'image-depth', 'depth', 'ushort', 'nearest')
+        this.depthTarget = this.packedDepth ? webgl.createRenderTarget(width, height) : null
+        this.depthTexture = this.depthTarget ? this.depthTarget.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest')
         if (!this.packedDepth) {
             this.depthTexture.define(width, height)
             this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth')
@@ -48,9 +48,13 @@ export class DrawPass {
             webgl.unbindFramebuffer()
         } else {
             colorTarget.bind()
+            if (!this.packedDepth) {
+                // TODO unlcear why it is not enough to call `attachFramebuffer` in `Texture.reset`
+                this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth')
+            }
         }
 
-        renderer.setViewport(0, 0, colorTarget.width, colorTarget.height)
+        renderer.setViewport(0, 0, colorTarget.getWidth(), colorTarget.getHeight())
         renderer.render(scene, camera, 'color', true, transparentBackground)
         if (debugHelper.isEnabled) {
             debugHelper.syncVisibility()

+ 9 - 7
src/mol-canvas3d/passes/multi-sample.ts

@@ -14,7 +14,7 @@ import { ShaderCode } from '../../mol-gl/shader-code';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { RenderTarget, createRenderTarget } from '../../mol-gl/webgl/render-target';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Camera } from '../../mol-canvas3d/camera';
 import { PostprocessingPass } from './postprocessing';
 import { DrawPass } from './draw';
@@ -35,7 +35,7 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): Compose
     const values: Values<typeof ComposeSchema> = {
         ...QuadValues,
         tColor: ValueCell.create(colorTexture),
-        uTexSize: ValueCell.create(Vec2.create(colorTexture.width, colorTexture.height)),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
         uWeight: ValueCell.create(1.0),
     }
 
@@ -66,9 +66,9 @@ export class MultiSamplePass {
 
     constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
         const { gl } = webgl
-        this.colorTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
-        this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
-        this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight)
         this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture)
         this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props }
     }
@@ -131,7 +131,8 @@ export class MultiSamplePass {
         ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture)
         compose.update()
 
-        const { width, height } = drawPass.colorTarget
+        const width = drawPass.colorTarget.getWidth()
+        const height = drawPass.colorTarget.getHeight()
 
         // render the scene multiple times, each slightly jitter offset
         // from the last and accumulate the results.
@@ -222,7 +223,8 @@ export class MultiSamplePass {
         ValueCell.update(compose.values.uWeight, sampleWeight)
         compose.update()
 
-        const { width, height } = drawPass.colorTarget
+        const width = drawPass.colorTarget.getWidth()
+        const height = drawPass.colorTarget.getHeight()
 
         // render the scene multiple times, each slightly jitter offset
         // from the last and accumulate the results.

+ 6 - 4
src/mol-canvas3d/passes/pick.ts

@@ -5,7 +5,7 @@
  */
 
 import { WebGLContext } from '../../mol-gl/webgl/context';
-import { createRenderTarget, RenderTarget } from '../../mol-gl/webgl/render-target';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { PickingId } from '../../mol-geo/geometry/picking';
@@ -36,9 +36,9 @@ export class PickPass {
         this.pickWidth = Math.round(width * this.pickScale)
         this.pickHeight = Math.round(height * this.pickScale)
 
-        this.objectPickTarget = createRenderTarget(webgl, this.pickWidth, this.pickHeight)
-        this.instancePickTarget = createRenderTarget(webgl, this.pickWidth, this.pickHeight)
-        this.groupPickTarget = createRenderTarget(webgl, this.pickWidth, this.pickHeight)
+        this.objectPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight)
+        this.instancePickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight)
+        this.groupPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight)
 
         this.setupBuffers()
     }
@@ -97,6 +97,8 @@ export class PickPass {
 
     identify(x: number, y: number): PickingId | undefined {
         const { webgl, pickScale } = this
+        if (webgl.isContextLost) return
+
         const { gl } = webgl
         if (this.pickDirty) {
             this.render()

+ 3 - 3
src/mol-canvas3d/passes/postprocessing.ts

@@ -14,7 +14,7 @@ import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { createRenderTarget, RenderTarget } from '../../mol-gl/webgl/render-target';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { DrawPass } from './draw';
 import { Camera } from '../../mol-canvas3d/camera';
 
@@ -66,7 +66,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         ...QuadValues,
         tColor: ValueCell.create(colorTexture),
         tDepth: ValueCell.create(depthTexture),
-        uTexSize: ValueCell.create(Vec2.create(colorTexture.width, colorTexture.height)),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
 
         dOrthographic: ValueCell.create(0),
         uNear: ValueCell.create(1),
@@ -101,7 +101,7 @@ export class PostprocessingPass {
 
     constructor(private webgl: WebGLContext, private camera: Camera, drawPass: DrawPass, props: Partial<PostprocessingProps>) {
         const { gl } = webgl
-        this.target = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight)
+        this.target = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight)
         this.props = { ...PD.getDefaultValues(PostprocessingParams), ...props }
         const { colorTarget, depthTexture, packedDepth } = drawPass
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, packedDepth, this.props)

+ 15 - 9
src/mol-data/db/column.ts

@@ -92,7 +92,13 @@ namespace Column {
         return !!v && !!(v as Column<any>).schema && !!(v as Column<any>).value;
     }
 
-    export const enum ValueKind { Present = 0, NotPresent = 1, Unknown = 2 }
+    export const enum ValueKind {
+        Present = 0,
+        /** Expressed in CIF as `.` */
+        NotPresent = 1,
+        /** Expressed in CIF as `?` */
+        Unknown = 2
+    }
 
     export function Undefined<T extends Schema>(rowCount: number, schema: T): Column<T['T']> {
         return constColumn(schema['T'], rowCount, schema, ValueKind.NotPresent);
@@ -290,14 +296,14 @@ function arrayColumn<T extends Column.Schema>({ array, schema, valueKind }: Colu
                 return ret;
             }
             : isTyped
-            ? params => ColumnHelpers.typedArrayWindow(array, params) as any as ReadonlyArray<T>
-            : params => {
-                const { start, end } = ColumnHelpers.getArrayBounds(rowCount, params);
-                if (start === 0 && end === array.length) return array as ReadonlyArray<T['T']>;
-                const ret = new (params && typeof params.array !== 'undefined' ? params.array : (array as any).constructor)(end - start) as any;
-                for (let i = 0, _i = end - start; i < _i; i++) ret[i] = array[start + i];
-                return ret;
-            },
+                ? params => ColumnHelpers.typedArrayWindow(array, params) as any as ReadonlyArray<T>
+                : params => {
+                    const { start, end } = ColumnHelpers.getArrayBounds(rowCount, params);
+                    if (start === 0 && end === array.length) return array as ReadonlyArray<T['T']>;
+                    const ret = new (params && typeof params.array !== 'undefined' ? params.array : (array as any).constructor)(end - start) as any;
+                    for (let i = 0, _i = end - start; i < _i; i++) ret[i] = array[start + i];
+                    return ret;
+                },
         areValuesEqual: (rowA, rowB) => array[rowA] === array[rowB]
     }
 }

+ 2 - 2
src/mol-data/generic/linked-list.ts

@@ -90,14 +90,14 @@ class LinkedListImpl<T> implements LinkedList<T> {
         if (node.previous !== null) {
             node.previous.next = node.next;
         }
-        else if (/*first == item*/ node.previous === null) {
+        else if (/* first == item*/ node.previous === null) {
             this.first = node.next;
         }
 
         if (node.next !== null) {
             node.next.previous = node.previous;
         }
-        else if (/*last == item*/ node.next === null) {
+        else if (/* last == item*/ node.next === null) {
             this.last = node.previous;
         }
 

+ 2 - 2
src/mol-data/util/_spec/interval-iterator.spec.ts

@@ -7,12 +7,12 @@
 import { Interval, OrderedSet, SortedArray } from '../../int';
 import { IntervalIterator } from '../interval-iterator';
 
- describe('interval', () => {
+describe('interval', () => {
     function testIterator(name: string, interval: Interval, set: OrderedSet, expectedValues: { index: number[], start: number[], end: number[]}) {
         it(`iterator, ${name}`, () => {
             const intervalIt = new IntervalIterator(interval, set)
             const { index, start, end } = expectedValues
-    
+
             let i = 0
             while (intervalIt.hasNext) {
                 const segment = intervalIt.move()

+ 67 - 27
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -18,7 +18,7 @@ import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
 import { GeometryUtils } from '../geometry';
 import { transformPositionArray } from '../../../mol-geo/util';
-import { calculateBoundingSphere } from '../../../mol-gl/renderable/util';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Theme } from '../../../mol-theme/theme';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { ColorListOptions, ColorListName } from '../../../mol-util/color/lists';
@@ -26,12 +26,14 @@ import { Color } from '../../../mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
 
 const VolumeBox = Box()
 const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][]
 
 export interface DirectVolume {
     readonly kind: 'direct-volume',
+
     readonly gridTexture: ValueCell<Texture>,
     readonly gridTextureDim: ValueCell<Vec3>,
     readonly gridDimension: ValueCell<Vec3>,
@@ -41,32 +43,66 @@ export interface DirectVolume {
     readonly transform: ValueCell<Mat4>
 
     /** Bounding sphere of the volume */
-    boundingSphere?: Sphere3D
+    boundingSphere: Sphere3D
 }
 
 export namespace DirectVolume {
     export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, directVolume?: DirectVolume): DirectVolume {
-        const { width, height, depth } = texture
-        if (directVolume) {
-            ValueCell.update(directVolume.gridDimension, gridDimension)
-            ValueCell.update(directVolume.gridTextureDim, Vec3.set(directVolume.gridTextureDim.ref.value, width, height, depth))
-            ValueCell.update(directVolume.bboxMin, bbox.min)
-            ValueCell.update(directVolume.bboxMax, bbox.max)
-            ValueCell.update(directVolume.bboxSize, Vec3.sub(directVolume.bboxSize.ref.value, bbox.max, bbox.min))
-            ValueCell.update(directVolume.transform, transform)
-            return directVolume
-        } else {
-            return {
-                kind: 'direct-volume',
-                gridDimension: ValueCell.create(gridDimension),
-                gridTexture: ValueCell.create(texture),
-                gridTextureDim: ValueCell.create(Vec3.create(width, height, depth)),
-                bboxMin: ValueCell.create(bbox.min),
-                bboxMax: ValueCell.create(bbox.max),
-                bboxSize: ValueCell.create(Vec3.sub(Vec3.zero(), bbox.max, bbox.min)),
-                transform: ValueCell.create(transform),
-            }
+        return directVolume ?
+            update(bbox, gridDimension, transform, texture, directVolume) :
+            fromData(bbox, gridDimension, transform, texture)
+    }
+
+    function hashCode(directVolume: DirectVolume) {
+        return hashFnv32a([
+            directVolume.bboxSize.ref.version, directVolume.gridDimension.ref.version,
+            directVolume.gridTexture.ref.version, directVolume.transform.ref.version,
+        ])
+    }
+
+    function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture): DirectVolume {
+        const boundingSphere = Sphere3D()
+        let currentHash = -1
+
+        const width = texture.getWidth()
+        const height = texture.getHeight()
+        const depth = texture.getDepth()
+
+        const directVolume = {
+            kind: 'direct-volume' as const,
+            gridDimension: ValueCell.create(gridDimension),
+            gridTexture: ValueCell.create(texture),
+            gridTextureDim: ValueCell.create(Vec3.create(width, height, depth)),
+            bboxMin: ValueCell.create(bbox.min),
+            bboxMax: ValueCell.create(bbox.max),
+            bboxSize: ValueCell.create(Vec3.sub(Vec3.zero(), bbox.max, bbox.min)),
+            transform: ValueCell.create(transform),
+            get boundingSphere() {
+                const newHash = hashCode(directVolume)
+                if (newHash !== currentHash) {
+                    const b = getBoundingSphere(directVolume.gridDimension.ref.value, directVolume.transform.ref.value)
+                    Sphere3D.copy(boundingSphere, b)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
         }
+        return directVolume
+    }
+
+    function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, directVolume: DirectVolume): DirectVolume {
+        const width = texture.getWidth()
+        const height = texture.getHeight()
+        const depth = texture.getDepth()
+
+        ValueCell.update(directVolume.gridDimension, gridDimension)
+        ValueCell.update(directVolume.gridTexture, texture)
+        ValueCell.update(directVolume.gridTextureDim, Vec3.set(directVolume.gridTextureDim.ref.value, width, height, depth))
+        ValueCell.update(directVolume.bboxMin, bbox.min)
+        ValueCell.update(directVolume.bboxMax, bbox.max)
+        ValueCell.update(directVolume.bboxSize, Vec3.sub(directVolume.bboxSize.ref.value, bbox.max, bbox.min))
+        ValueCell.update(directVolume.transform, transform)
+        return directVolume
     }
 
     export function createEmpty(directVolume?: DirectVolume): DirectVolume {
@@ -108,7 +144,8 @@ export namespace DirectVolume {
 
         const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount }
 
-        const { boundingSphere, invariantBoundingSphere } = getBoundingSphere(gridDimension.ref.value, gridTransform.ref.value, transform.aTransform.ref.value, transform.instanceCount.ref.value)
+        const invariantBoundingSphere = Sphere3D.clone(directVolume.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
         const transferTex = createTransferFunctionTexture(controlPoints, props.list)
@@ -138,7 +175,7 @@ export namespace DirectVolume {
             dRenderMode: ValueCell.create(props.renderMode),
             tTransferTex: transferTex,
 
-            dGridTexType: ValueCell.create(gridTexture.ref.value.depth > 0 ? '3d' : '2d'),
+            dGridTexType: ValueCell.create(gridTexture.ref.value.getDepth() > 0 ? '3d' : '2d'),
             uGridTexDim: gridTextureDim,
             tGridTex: gridTexture,
         }
@@ -160,7 +197,9 @@ export namespace DirectVolume {
     }
 
     function updateBoundingSphere(values: DirectVolumeValues, directVolume: DirectVolume) {
-        const { boundingSphere, invariantBoundingSphere } = getBoundingSphere(values.uGridDim.ref.value, values.uTransform.ref.value, values.aTransform.ref.value, values.instanceCount.ref.value)
+        const invariantBoundingSphere = Sphere3D.clone(directVolume.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
@@ -187,11 +226,12 @@ const mTmp = Mat4.identity()
 const mTmp2 = Mat4.identity()
 const vHalfUnit = Vec3.create(0.5, 0.5, 0.5)
 const tmpVertices = new Float32Array(VolumeBox.vertices.length)
-function getBoundingSphere(gridDimension: Vec3, gridTransform: Mat4, transform: Float32Array, transformCount: number) {
+function getBoundingSphere(gridDimension: Vec3, gridTransform: Mat4) {
     tmpVertices.set(VolumeBox.vertices)
     Mat4.fromTranslation(mTmp, vHalfUnit)
     Mat4.mul(mTmp, Mat4.fromScaling(mTmp2, gridDimension), mTmp)
     Mat4.mul(mTmp, gridTransform, mTmp)
     transformPositionArray(mTmp, tmpVertices, 0, tmpVertices.length / 3)
-    return calculateBoundingSphere(tmpVertices, tmpVertices.length / 3, transform, transformCount)
+    return calculateInvariantBoundingSphere(tmpVertices, tmpVertices.length / 3, 1)
+    // return calculateBoundingSphere(tmpVertices, tmpVertices.length / 3, transform, transformCount)
 }

+ 25 - 28
src/mol-geo/geometry/geometry.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -19,32 +19,30 @@ import { Spheres } from './spheres/spheres';
 import { arrayMax } from '../../mol-util/array';
 import { TransformData } from './transform-data';
 import { Theme } from '../../mol-theme/theme';
-import { RenderObjectValuesType } from '../../mol-gl/render-object';
-import { ValueOf } from '../../mol-util/type-helpers';
+import { RenderObjectValues } from '../../mol-gl/render-object';
 import { TextureMesh } from './texture-mesh/texture-mesh';
 
-export type GeometryKindType = {
-    'mesh': Mesh,
-    'points': Points,
-    'spheres': Spheres,
-    'text': Text,
-    'lines': Lines,
-    'direct-volume': DirectVolume,
-    'texture-mesh': TextureMesh,
-}
-export type GeometryKindParams = {
-    'mesh': Mesh.Params,
-    'points': Points.Params,
-    'spheres': Spheres.Params,
-    'text': Text.Params,
-    'lines': Lines.Params,
-    'direct-volume': DirectVolume.Params,
-    'texture-mesh': TextureMesh.Params,
-}
-export type GeometryKind = keyof GeometryKindType
-export type Geometry = ValueOf<GeometryKindType>
+export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'texture-mesh'
+
+export type Geometry<T extends GeometryKind = GeometryKind> =
+    T extends 'mesh' ? Mesh :
+        T extends 'points' ? Points :
+            T extends 'spheres' ? Spheres :
+                T extends 'text' ? Text :
+                    T extends 'lines' ? Lines :
+                        T extends 'direct-volume' ? DirectVolume :
+                            T extends 'texture-mesh' ? TextureMesh : never
+
+type GeometryParams<T extends GeometryKind> =
+    T extends 'mesh' ? Mesh.Params :
+        T extends 'points' ? Points.Params :
+            T extends 'spheres' ? Spheres.Params :
+                T extends 'text' ? Text.Params :
+                    T extends 'lines' ? Lines.Params :
+                        T extends 'direct-volume' ? DirectVolume.Params :
+                            T extends 'texture-mesh' ? TextureMesh.Params : never
 
-export interface GeometryUtils<G extends Geometry, P extends PD.Params = GeometryKindParams[G['kind']], V = RenderObjectValuesType[G['kind']]> {
+export interface GeometryUtils<G extends Geometry, P extends PD.Params = GeometryParams<G['kind']>, V = RenderObjectValues<G['kind']>> {
     Params: P
     createEmpty(geometry?: G): G
     createValues(geometry: G, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<P>): V
@@ -56,7 +54,7 @@ export interface GeometryUtils<G extends Geometry, P extends PD.Params = Geometr
 }
 
 export namespace Geometry {
-    export type Params<G extends Geometry> = GeometryKindParams[G['kind']]
+    export type Params<G extends Geometry> = GeometryParams<G['kind']>
 
     export function getDrawCount(geometry: Geometry): number {
         switch (geometry.kind) {
@@ -66,7 +64,7 @@ export namespace Geometry {
             case 'text': return geometry.charCount * 2 * 3
             case 'lines': return geometry.lineCount * 2 * 3
             case 'direct-volume': return 12 * 3
-            case 'texture-mesh': return geometry.vertexCount.ref.value
+            case 'texture-mesh': return geometry.vertexCount
         }
     }
 
@@ -81,7 +79,7 @@ export namespace Geometry {
             case 'direct-volume':
                 return 1
             case 'texture-mesh':
-                return geometry.groupCount.ref.value
+                return geometry.groupCount
         }
     }
 
@@ -96,7 +94,6 @@ export namespace Geometry {
             case 'direct-volume': return DirectVolume.Utils as any
             case 'texture-mesh': return TextureMesh.Utils as any
         }
-        throw new Error('unknown geometry kind')
     }
 
     export function getGranularity(locationIt: LocationIterator, granularity: ColorType | SizeType) {

+ 2 - 11
src/mol-geo/geometry/lines/lines-builder.ts

@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from '../../../mol-util/value-cell'
 import { ChunkedArray } from '../../../mol-data/util';
 import { Lines } from './lines';
 import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
@@ -84,15 +83,7 @@ export namespace LinesBuilder {
                 const gb = ChunkedArray.compact(groups, true) as Float32Array
                 const sb = ChunkedArray.compact(starts, true) as Float32Array
                 const eb = ChunkedArray.compact(ends, true) as Float32Array
-                return {
-                    kind: 'lines',
-                    lineCount: indices.elementCount / 2,
-                    mappingBuffer: lines ? ValueCell.update(lines.mappingBuffer, mb) : ValueCell.create(mb),
-                    indexBuffer: lines ? ValueCell.update(lines.indexBuffer, ib) : ValueCell.create(ib),
-                    groupBuffer: lines ? ValueCell.update(lines.groupBuffer, gb) : ValueCell.create(gb),
-                    startBuffer: lines ? ValueCell.update(lines.startBuffer, sb) : ValueCell.create(sb),
-                    endBuffer: lines ? ValueCell.update(lines.endBuffer, eb) : ValueCell.create(eb),
-                }
+                return Lines.create(mb, ib, gb, sb, eb, indices.elementCount / 2)
             }
         }
     }

+ 81 - 32
src/mol-geo/geometry/lines/lines.ts

@@ -1,12 +1,14 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ValueCell } from '../../../mol-util'
 import { Mat4 } from '../../../mol-math/linear-algebra'
-import { transformPositionArray/* , transformDirectionArray, getNormalMatrix */ } from '../../util';
+import { transformPositionArray,/* , transformDirectionArray, getNormalMatrix */
+GroupMapping,
+createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
@@ -17,19 +19,22 @@ import { LinesValues } from '../../../mol-gl/renderable/lines';
 import { Mesh } from '../mesh/mesh';
 import { LinesBuilder } from './lines-builder';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { calculateBoundingSphere } from '../../../mol-gl/renderable/util';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Theme } from '../../../mol-theme/theme';
 import { Color } from '../../../mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
 
 /** Wide line */
 export interface Lines {
     readonly kind: 'lines',
+
     /** Number of lines */
     lineCount: number,
+
     /** Mapping buffer as array of xy values wrapped in a value cell */
     readonly mappingBuffer: ValueCell<Float32Array>,
     /** Index buffer as array of vertex index triplets wrapped in a value cell */
@@ -40,24 +45,27 @@ export interface Lines {
     readonly startBuffer: ValueCell<Float32Array>,
     /** Line end buffer as array of xyz values wrapped in a value cell */
     readonly endBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the lines */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to line indices */
+    readonly groupMapping: GroupMapping
 }
 
 export namespace Lines {
+    export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines?: Lines): Lines {
+        return lines ?
+            update(mappings, indices, groups, starts, ends, lineCount, lines) :
+            fromArrays(mappings, indices, groups, starts, ends, lineCount)
+    }
+
     export function createEmpty(lines?: Lines): Lines {
         const mb = lines ? lines.mappingBuffer.ref.value : new Float32Array(0)
         const ib = lines ? lines.indexBuffer.ref.value : new Uint32Array(0)
         const gb = lines ? lines.groupBuffer.ref.value : new Float32Array(0)
         const sb = lines ? lines.startBuffer.ref.value : new Float32Array(0)
         const eb = lines ? lines.endBuffer.ref.value : new Float32Array(0)
-        return {
-            kind: 'lines',
-            lineCount: 0,
-            mappingBuffer: lines ? ValueCell.update(lines.mappingBuffer, mb) : ValueCell.create(mb),
-            indexBuffer: lines ? ValueCell.update(lines.indexBuffer, ib) : ValueCell.create(ib),
-            groupBuffer: lines ? ValueCell.update(lines.groupBuffer, gb) : ValueCell.create(gb),
-            startBuffer: lines ? ValueCell.update(lines.startBuffer, sb) : ValueCell.create(sb),
-            endBuffer: lines ? ValueCell.update(lines.endBuffer, eb) : ValueCell.create(eb),
-        }
+        return create(mb, ib, gb, sb, eb, 0, lines)
     }
 
     export function fromMesh(mesh: Mesh, lines?: Lines) {
@@ -81,16 +89,67 @@ export namespace Lines {
         return builder.getLines();
     }
 
-    export function transformImmediate(line: Lines, t: Mat4) {
-        transformRangeImmediate(line, t, 0, line.lineCount)
+    function hashCode(lines: Lines) {
+        return hashFnv32a([
+            lines.lineCount, lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
+            lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.startBuffer.ref.version
+        ])
+    }
+
+    function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number): Lines {
+
+        const boundingSphere = Sphere3D()
+        let groupMapping: GroupMapping
+
+        let currentHash = -1
+        let currentGroup = -1
+
+        const lines = {
+            kind: 'lines' as const,
+            lineCount,
+            mappingBuffer: ValueCell.create(mappings),
+            indexBuffer: ValueCell.create(indices),
+            groupBuffer: ValueCell.create(groups),
+            startBuffer: ValueCell.create(starts),
+            endBuffer: ValueCell.create(ends),
+            get boundingSphere() {
+                const newHash = hashCode(lines)
+                if (newHash !== currentHash) {
+                    const s = calculateInvariantBoundingSphere(lines.startBuffer.ref.value, lines.lineCount * 4, 4)
+                    const e = calculateInvariantBoundingSphere(lines.endBuffer.ref.value, lines.lineCount * 4, 4)
+
+                    Sphere3D.expandBySphere(boundingSphere, s, e)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
+            get groupMapping() {
+                if (lines.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(lines.groupBuffer.ref.value, lines.lineCount, 4)
+                    currentGroup = lines.groupBuffer.ref.version
+                }
+                return groupMapping
+            }
+        }
+        return lines
+    }
+
+    function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines: Lines) {
+        lines.lineCount = lineCount
+        ValueCell.update(lines.mappingBuffer, mappings)
+        ValueCell.update(lines.indexBuffer, indices)
+        ValueCell.update(lines.groupBuffer, groups)
+        ValueCell.update(lines.startBuffer, starts)
+        ValueCell.update(lines.endBuffer, ends)
+        return lines
     }
 
-    export function transformRangeImmediate(lines: Lines, t: Mat4, offset: number, count: number) {
+    export function transform(lines: Lines, t: Mat4) {
         const start = lines.startBuffer.ref.value
-        transformPositionArray(t, start, offset, count * 4)
+        transformPositionArray(t, start, 0, lines.lineCount * 4)
         ValueCell.update(lines.startBuffer, start);
         const end = lines.endBuffer.ref.value
-        transformPositionArray(t, end, offset, count * 4)
+        transformPositionArray(t, end, 0, lines.lineCount * 4)
         ValueCell.update(lines.endBuffer, end);
     }
 
@@ -124,8 +183,8 @@ export namespace Lines {
 
         const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount }
 
-        const { boundingSphere, invariantBoundingSphere } = getBoundingSphere(lines.startBuffer.ref.value, lines.endBuffer.ref.value, lines.lineCount,
-            transform.aTransform.ref.value, transform.instanceCount.ref.value)
+        const invariantBoundingSphere = Sphere3D.clone(lines.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         return {
             aMapping: lines.mappingBuffer,
@@ -163,10 +222,9 @@ export namespace Lines {
     }
 
     function updateBoundingSphere(values: LinesValues, lines: Lines) {
-        const { boundingSphere, invariantBoundingSphere } = getBoundingSphere(
-            values.aStart.ref.value, values.aEnd.ref.value, lines.lineCount,
-            values.aTransform.ref.value, values.instanceCount.ref.value
-        )
+        const invariantBoundingSphere = Sphere3D.clone(lines.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
@@ -174,13 +232,4 @@ export namespace Lines {
             ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere)
         }
     }
-}
-
-function getBoundingSphere(lineStart: Float32Array, lineEnd: Float32Array, lineCount: number, transform: Float32Array, transformCount: number) {
-    const start = calculateBoundingSphere(lineStart, lineCount * 4, transform, transformCount, 0, 4)
-    const end = calculateBoundingSphere(lineEnd, lineCount * 4, transform, transformCount, 0, 4)
-    return {
-        boundingSphere: Sphere3D.expandBySphere(start.boundingSphere, end.boundingSphere),
-        invariantBoundingSphere: Sphere3D.expandBySphere(start.invariantBoundingSphere, end.invariantBoundingSphere)
-    }
 }

+ 90 - 0
src/mol-geo/geometry/mesh/laplacian-smoothing.ts

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+// TODO
+
+//     function addVertex(src: Float32Array, i: number, dst: Float32Array, j: number) {
+//         dst[3 * j] += src[3 * i];
+//         dst[3 * j + 1] += src[3 * i + 1];
+//         dst[3 * j + 2] += src[3 * i + 2];
+//     }
+
+//     function laplacianSmoothIter(surface: Surface, vertexCounts: Int32Array, vs: Float32Array, vertexWeight: number) {
+//         const triCount = surface.triangleIndices.length,
+//             src = surface.vertices;
+
+//         const triangleIndices = surface.triangleIndices;
+
+//         for (let i = 0; i < triCount; i += 3) {
+//             const a = triangleIndices[i],
+//                 b = triangleIndices[i + 1],
+//                 c = triangleIndices[i + 2];
+
+//             addVertex(src, b, vs, a);
+//             addVertex(src, c, vs, a);
+
+//             addVertex(src, a, vs, b);
+//             addVertex(src, c, vs, b);
+
+//             addVertex(src, a, vs, c);
+//             addVertex(src, b, vs, c);
+//         }
+
+//         const vw = 2 * vertexWeight;
+//         for (let i = 0, _b = surface.vertexCount; i < _b; i++) {
+//             const n = vertexCounts[i] + vw;
+//             vs[3 * i] = (vs[3 * i] + vw * src[3 * i]) / n;
+//             vs[3 * i + 1] = (vs[3 * i + 1] + vw * src[3 * i + 1]) / n;
+//             vs[3 * i + 2] = (vs[3 * i + 2] + vw * src[3 * i + 2]) / n;
+//         }
+//     }
+
+//     async function laplacianSmoothComputation(ctx: Computation.Context, surface: Surface, iterCount: number, vertexWeight: number) {
+//         await ctx.updateProgress('Smoothing surface...', true);
+
+//         const vertexCounts = new Int32Array(surface.vertexCount),
+//             triCount = surface.triangleIndices.length;
+
+//         const tris = surface.triangleIndices;
+//         for (let i = 0; i < triCount; i++) {
+//             // in a triangle 2 edges touch each vertex, hence the constant.
+//             vertexCounts[tris[i]] += 2;
+//         }
+
+//         let vs = new Float32Array(surface.vertices.length);
+//         let started = Utils.PerformanceMonitor.currentTime();
+//         await ctx.updateProgress('Smoothing surface...', true);
+//         for (let i = 0; i < iterCount; i++) {
+//             if (i > 0) {
+//                 for (let j = 0, _b = vs.length; j < _b; j++) vs[j] = 0;
+//             }
+//             surface.normals = void 0;
+//             laplacianSmoothIter(surface, vertexCounts, vs, vertexWeight);
+//             const t = surface.vertices;
+//             surface.vertices = <any>vs;
+//             vs = <any>t;
+
+//             const time = Utils.PerformanceMonitor.currentTime();
+//             if (time - started > Computation.UpdateProgressDelta) {
+//                 started = time;
+//                 await ctx.updateProgress('Smoothing surface...', true, i + 1, iterCount);
+//             }
+//         }
+//         return surface;
+//     }
+
+//     /*
+//      * Smooths the vertices by averaging the neighborhood.
+//      *
+//      * Resets normals. Might replace vertex array.
+//      */
+//     export function laplacianSmooth(surface: Surface, iterCount: number = 1, vertexWeight: number = 1): Computation<Surface> {
+
+//         if (iterCount < 1) iterCount = 0;
+//         if (iterCount === 0) return Computation.resolve(surface);
+
+//         return computation(async ctx => await laplacianSmoothComputation(ctx, surface, iterCount, (1.1 * vertexWeight) / 1.1));
+//     }

+ 2 - 12
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from '../../../mol-util/value-cell'
 import { Vec3, Mat4, Mat3 } from '../../../mol-math/linear-algebra';
 import { ChunkedArray } from '../../../mol-data/util';
 import { Mesh } from './mesh';
@@ -141,15 +140,6 @@ export namespace MeshBuilder {
         const ib = ChunkedArray.compact(indices, true) as Uint32Array
         const nb = ChunkedArray.compact(normals, true) as Float32Array
         const gb = ChunkedArray.compact(groups, true) as Float32Array
-        return {
-            kind: 'mesh',
-            vertexCount: state.vertices.elementCount,
-            triangleCount: state.indices.elementCount,
-            vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
-            indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
-            normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
-            groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
-            normalsComputed: true,
-        }
+        return Mesh.create(vb, ib, nb, gb, state.vertices.elementCount, state.indices.elementCount, mesh)
     }
 }

+ 88 - 211
src/mol-geo/geometry/mesh/mesh.ts

@@ -1,22 +1,22 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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 { Task } from '../../../mol-task'
 import { ValueCell } from '../../../mol-util'
-import { Vec3, Mat4 } from '../../../mol-math/linear-algebra'
+import { Vec3, Mat4, Mat3 } from '../../../mol-math/linear-algebra'
 import { Sphere3D } from '../../../mol-math/geometry'
-import { transformPositionArray/* , transformDirectionArray, getNormalMatrix */ } from '../../util';
+import { transformPositionArray, transformDirectionArray, computeIndexedVertexNormals, GroupMapping, createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
 import { createMarkers } from '../marker-data';
 import { TransformData } from '../transform-data';
 import { LocationIterator } from '../../util/location-iterator';
 import { createColors } from '../color-data';
-import { ChunkedArray } from '../../../mol-data/util';
+import { ChunkedArray, hashFnv32a } from '../../../mol-data/util';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { calculateBoundingSphere } from '../../../mol-gl/renderable/util';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Theme } from '../../../mol-theme/theme';
 import { MeshValues } from '../../../mol-gl/renderable/mesh';
 import { Color } from '../../../mol-util/color';
@@ -41,85 +41,96 @@ export interface Mesh {
     /** Group buffer as array of group ids for each vertex wrapped in a value cell */
     readonly groupBuffer: ValueCell<Float32Array>,
 
-    /** Flag indicating if normals are computed for the current set of vertices */
-    normalsComputed: boolean,
-
     /** Bounding sphere of the mesh */
-    boundingSphere?: Sphere3D
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to vertex indices */
+    readonly groupMapping: GroupMapping
 }
 
 export namespace Mesh {
+    export function create(vertices: Float32Array, indices: Uint32Array, normals: Float32Array, groups: Float32Array, vertexCount: number, triangleCount: number, mesh?: Mesh): Mesh {
+        return mesh ?
+            update(vertices, indices, normals, groups, vertexCount, triangleCount, mesh) :
+            fromArrays(vertices, indices, normals, groups, vertexCount, triangleCount)
+    }
+
     export function createEmpty(mesh?: Mesh): Mesh {
         const vb = mesh ? mesh.vertexBuffer.ref.value : new Float32Array(0)
         const ib = mesh ? mesh.indexBuffer.ref.value : new Uint32Array(0)
         const nb = mesh ? mesh.normalBuffer.ref.value : new Float32Array(0)
         const gb = mesh ? mesh.groupBuffer.ref.value : new Float32Array(0)
-        return {
-            kind: 'mesh',
-            vertexCount: 0,
-            triangleCount: 0,
-            vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
-            indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
-            normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
-            groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
-            normalsComputed: true,
-        }
+        return create(vb, ib, nb, gb, 0, 0, mesh)
     }
 
-    export function fromArrays(vertices: Float32Array, indices: Uint32Array, normals: Float32Array, groups: Float32Array, vertexCount: number, triangleCount: number, normalsComputed: boolean): Mesh {
-        return {
-            kind: 'mesh',
+    function hashCode(mesh: Mesh) {
+        return hashFnv32a([
+            mesh.vertexCount, mesh.triangleCount,
+            mesh.vertexBuffer.ref.version, mesh.indexBuffer.ref.version,
+            mesh.normalBuffer.ref.version, mesh.groupBuffer.ref.version
+        ])
+    }
+
+    function fromArrays(vertices: Float32Array, indices: Uint32Array, normals: Float32Array, groups: Float32Array, vertexCount: number, triangleCount: number): Mesh {
+
+        const boundingSphere = Sphere3D()
+        let groupMapping: GroupMapping
+
+        let currentHash = -1
+        let currentGroup = -1
+
+        const mesh = {
+            kind: 'mesh' as const,
             vertexCount,
             triangleCount,
             vertexBuffer: ValueCell.create(vertices),
             indexBuffer: ValueCell.create(indices),
             normalBuffer: ValueCell.create(normals),
             groupBuffer: ValueCell.create(groups),
-            normalsComputed,
+            get boundingSphere() {
+                const newHash = hashCode(mesh)
+                if (newHash !== currentHash) {
+                    const b = calculateInvariantBoundingSphere(mesh.vertexBuffer.ref.value, mesh.vertexCount, 1)
+                    Sphere3D.copy(boundingSphere, b)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
+            get groupMapping() {
+                if (mesh.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(mesh.groupBuffer.ref.value, mesh.vertexCount)
+                    currentGroup = mesh.groupBuffer.ref.version
+                }
+                return groupMapping
+            }
         }
+        return mesh
     }
 
-    export function computeNormalsImmediate(mesh: Mesh) {
-        if (mesh.normalsComputed) return;
+    function update(vertices: Float32Array, indices: Uint32Array, normals: Float32Array, groups: Float32Array, vertexCount: number, triangleCount: number, mesh: Mesh) {
+        mesh.vertexCount = vertexCount
+        mesh.triangleCount = triangleCount
+        ValueCell.update(mesh.vertexBuffer, vertices)
+        ValueCell.update(mesh.indexBuffer, indices)
+        ValueCell.update(mesh.normalBuffer, normals)
+        ValueCell.update(mesh.groupBuffer, groups)
+        return mesh
+    }
 
-        const normals = mesh.normalBuffer.ref.value.length >= mesh.vertexCount * 3
-            ? mesh.normalBuffer.ref.value : new Float32Array(mesh.vertexBuffer.ref.value.length);
+    export function computeNormals(mesh: Mesh) {
+        const { vertexCount, triangleCount } = mesh
+        const vertices = mesh.vertexBuffer.ref.value
+        const indices = mesh.indexBuffer.ref.value
 
-        const v = mesh.vertexBuffer.ref.value, triangles = mesh.indexBuffer.ref.value;
+        const normals = mesh.normalBuffer.ref.value.length >= vertexCount * 3
+            ? mesh.normalBuffer.ref.value
+            : new Float32Array(vertexCount * 3);
 
         if (normals === mesh.normalBuffer.ref.value) {
-            for (let i = 0, ii = 3 * mesh.vertexCount; i < ii; i += 3) {
-                normals[i] = 0; normals[i + 1] = 0; normals[i + 2] = 0;
-            }
-        }
-
-        const x = Vec3.zero(), y = Vec3.zero(), z = Vec3.zero(), d1 = Vec3.zero(), d2 = Vec3.zero(), n = Vec3.zero();
-        for (let i = 0, ii = 3 * mesh.triangleCount; i < ii; i += 3) {
-            const a = 3 * triangles[i], b = 3 * triangles[i + 1], c = 3 * triangles[i + 2];
-
-            Vec3.fromArray(x, v, a);
-            Vec3.fromArray(y, v, b);
-            Vec3.fromArray(z, v, c);
-            Vec3.sub(d1, z, y);
-            Vec3.sub(d2, x, y);
-            Vec3.cross(n, d1, d2);
-
-            normals[a] += n[0]; normals[a + 1] += n[1]; normals[a + 2] += n[2];
-            normals[b] += n[0]; normals[b + 1] += n[1]; normals[b + 2] += n[2];
-            normals[c] += n[0]; normals[c + 1] += n[1]; normals[c + 2] += n[2];
+            normals.fill(0, 0, vertexCount * 3)
         }
 
-        for (let i = 0, ii = 3 * mesh.vertexCount; i < ii; i += 3) {
-            const nx = normals[i];
-            const ny = normals[i + 1];
-            const nz = normals[i + 2];
-            const f = 1.0 / Math.sqrt(nx * nx + ny * ny + nz * nz);
-            normals[i] *= f; normals[i + 1] *= f; normals[i + 2] *= f;
-
-            // console.log([normals[i], normals[i + 1], normals[i + 2]], [v[i], v[i + 1], v[i + 2]])
-        }
+        computeIndexedVertexNormals(vertices, indices, normals, vertexCount, triangleCount)
         ValueCell.update(mesh.normalBuffer, normals);
-        mesh.normalsComputed = true;
     }
 
     export function checkForDuplicateVertices(mesh: Mesh, fractionDigits = 3) {
@@ -129,7 +140,7 @@ export namespace Mesh {
         const hash = (v: Vec3, d: number) => `${v[0].toFixed(d)}|${v[1].toFixed(d)}|${v[2].toFixed(d)}`
         let duplicates = 0
 
-        const a = Vec3.zero()
+        const a = Vec3()
         for (let i = 0, il = mesh.vertexCount; i < il; ++i) {
             Vec3.fromArray(a, v, i * 3)
             const k = hash(a, fractionDigits)
@@ -144,63 +155,15 @@ export namespace Mesh {
         return duplicates
     }
 
-    export function computeNormals(surface: Mesh): Task<Mesh> {
-        return Task.create<Mesh>('Surface (Compute Normals)', async ctx => {
-            if (surface.normalsComputed) return surface;
-
-            await ctx.update('Computing normals...');
-            computeNormalsImmediate(surface);
-            return surface;
-        });
-    }
-
-    export function transformImmediate(mesh: Mesh, t: Mat4) {
-        transformRangeImmediate(mesh, t, 0, mesh.vertexCount)
-    }
-
-    export function transformRangeImmediate(mesh: Mesh, t: Mat4, offset: number, count: number) {
+    const tmpMat3 = Mat3()
+    export function transform(mesh: Mesh, t: Mat4) {
         const v = mesh.vertexBuffer.ref.value
-        transformPositionArray(t, v, offset, count)
-        // TODO normals transformation does not work for an unknown reason, ASR
-        // if (mesh.normalBuffer.ref.value) {
-        //     const n = getNormalMatrix(Mat3.zero(), t)
-        //     transformDirectionArray(n, mesh.normalBuffer.ref.value, offset, count)
-        //     mesh.normalsComputed = true;
-        // }
+        transformPositionArray(t, v, 0, mesh.vertexCount)
+        if (!Mat4.isTranslationAndUniformScaling(t)) {
+            const n = Mat3.directionTransform(tmpMat3, t)
+            transformDirectionArray(n, mesh.normalBuffer.ref.value, 0, mesh.vertexCount)
+        }
         ValueCell.update(mesh.vertexBuffer, v);
-        mesh.normalsComputed = false;
-    }
-
-    export function computeBoundingSphere(mesh: Mesh): Task<Mesh> {
-        return Task.create<Mesh>('Mesh (Compute Bounding Sphere)', async ctx => {
-            if (mesh.boundingSphere) {
-                return mesh;
-            }
-            await ctx.update('Computing bounding sphere...');
-
-            const vertices = mesh.vertexBuffer.ref.value;
-            let x = 0, y = 0, z = 0;
-            for (let i = 0, _c = vertices.length; i < _c; i += 3) {
-                x += vertices[i];
-                y += vertices[i + 1];
-                z += vertices[i + 2];
-            }
-            x /= mesh.vertexCount;
-            y /= mesh.vertexCount;
-            z /= mesh.vertexCount;
-            let r = 0;
-            for (let i = 0, _c = vertices.length; i < _c; i += 3) {
-                const dx = x - vertices[i];
-                const dy = y - vertices[i + 1];
-                const dz = z - vertices[i + 2];
-                r = Math.max(r, dx * dx + dy * dy + dz * dz);
-            }
-            mesh.boundingSphere = {
-                center: Vec3.create(x, y, z),
-                radius: Math.sqrt(r)
-            }
-            return mesh;
-        });
     }
 
     /**
@@ -228,12 +191,12 @@ export namespace Mesh {
         group.currentIndex = vertexCount
         group.elementCount = vertexCount
 
-        const vi = Vec3.zero()
-        const vj = Vec3.zero()
-        const vk = Vec3.zero()
-        const ni = Vec3.zero()
-        const nj = Vec3.zero()
-        const nk = Vec3.zero()
+        const vi = Vec3()
+        const vj = Vec3()
+        const vk = Vec3()
+        const ni = Vec3()
+        const nj = Vec3()
+        const nk = Vec3()
 
         function add(i: number) {
             Vec3.fromArray(vi, vb, i * 3)
@@ -389,10 +352,8 @@ export namespace Mesh {
 
         const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
 
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            mesh.vertexBuffer.ref.value, mesh.vertexCount,
-            transform.aTransform.ref.value, instanceCount
-        )
+        const invariantBoundingSphere = Sphere3D.clone(mesh.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         return {
             aPosition: mesh.vertexBuffer,
@@ -430,10 +391,9 @@ export namespace Mesh {
     }
 
     function updateBoundingSphere(values: MeshValues, mesh: Mesh) {
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            values.aPosition.ref.value, mesh.vertexCount,
-            values.aTransform.ref.value, values.instanceCount.ref.value
-        )
+        const invariantBoundingSphere = Sphere3D.clone(mesh.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
@@ -442,86 +402,3 @@ export namespace Mesh {
         }
     }
 }
-
-//     function addVertex(src: Float32Array, i: number, dst: Float32Array, j: number) {
-//         dst[3 * j] += src[3 * i];
-//         dst[3 * j + 1] += src[3 * i + 1];
-//         dst[3 * j + 2] += src[3 * i + 2];
-//     }
-
-//     function laplacianSmoothIter(surface: Surface, vertexCounts: Int32Array, vs: Float32Array, vertexWeight: number) {
-//         const triCount = surface.triangleIndices.length,
-//             src = surface.vertices;
-
-//         const triangleIndices = surface.triangleIndices;
-
-//         for (let i = 0; i < triCount; i += 3) {
-//             const a = triangleIndices[i],
-//                 b = triangleIndices[i + 1],
-//                 c = triangleIndices[i + 2];
-
-//             addVertex(src, b, vs, a);
-//             addVertex(src, c, vs, a);
-
-//             addVertex(src, a, vs, b);
-//             addVertex(src, c, vs, b);
-
-//             addVertex(src, a, vs, c);
-//             addVertex(src, b, vs, c);
-//         }
-
-//         const vw = 2 * vertexWeight;
-//         for (let i = 0, _b = surface.vertexCount; i < _b; i++) {
-//             const n = vertexCounts[i] + vw;
-//             vs[3 * i] = (vs[3 * i] + vw * src[3 * i]) / n;
-//             vs[3 * i + 1] = (vs[3 * i + 1] + vw * src[3 * i + 1]) / n;
-//             vs[3 * i + 2] = (vs[3 * i + 2] + vw * src[3 * i + 2]) / n;
-//         }
-//     }
-
-//     async function laplacianSmoothComputation(ctx: Computation.Context, surface: Surface, iterCount: number, vertexWeight: number) {
-//         await ctx.updateProgress('Smoothing surface...', true);
-
-//         const vertexCounts = new Int32Array(surface.vertexCount),
-//             triCount = surface.triangleIndices.length;
-
-//         const tris = surface.triangleIndices;
-//         for (let i = 0; i < triCount; i++) {
-//             // in a triangle 2 edges touch each vertex, hence the constant.
-//             vertexCounts[tris[i]] += 2;
-//         }
-
-//         let vs = new Float32Array(surface.vertices.length);
-//         let started = Utils.PerformanceMonitor.currentTime();
-//         await ctx.updateProgress('Smoothing surface...', true);
-//         for (let i = 0; i < iterCount; i++) {
-//             if (i > 0) {
-//                 for (let j = 0, _b = vs.length; j < _b; j++) vs[j] = 0;
-//             }
-//             surface.normals = void 0;
-//             laplacianSmoothIter(surface, vertexCounts, vs, vertexWeight);
-//             const t = surface.vertices;
-//             surface.vertices = <any>vs;
-//             vs = <any>t;
-
-//             const time = Utils.PerformanceMonitor.currentTime();
-//             if (time - started > Computation.UpdateProgressDelta) {
-//                 started = time;
-//                 await ctx.updateProgress('Smoothing surface...', true, i + 1, iterCount);
-//             }
-//         }
-//         return surface;
-//     }
-
-//     /*
-//      * Smooths the vertices by averaging the neighborhood.
-//      *
-//      * Resets normals. Might replace vertex array.
-//      */
-//     export function laplacianSmooth(surface: Surface, iterCount: number = 1, vertexWeight: number = 1): Computation<Surface> {
-
-//         if (iterCount < 1) iterCount = 0;
-//         if (iterCount === 0) return Computation.resolve(surface);
-
-//         return computation(async ctx => await laplacianSmoothComputation(ctx, surface, iterCount, (1.1 * vertexWeight) / 1.1));
-//     }

+ 2 - 8
src/mol-geo/geometry/points/points-builder.ts

@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from '../../../mol-util/value-cell'
 import { ChunkedArray } from '../../../mol-data/util';
 import { Points } from './points';
 
@@ -26,12 +25,7 @@ export namespace PointsBuilder {
             getPoints: () => {
                 const cb = ChunkedArray.compact(centers, true) as Float32Array
                 const gb = ChunkedArray.compact(groups, true) as Float32Array
-                return {
-                    kind: 'points',
-                    pointCount: centers.elementCount,
-                    centerBuffer: points ? ValueCell.update(points.centerBuffer, cb) : ValueCell.create(cb),
-                    groupBuffer: points ? ValueCell.update(points.groupBuffer, gb) : ValueCell.create(gb),
-                }
+                return Points.create(cb, gb, centers.elementCount, points)
             }
         }
     }

+ 70 - 20
src/mol-geo/geometry/points/points.ts

@@ -1,12 +1,14 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ValueCell } from '../../../mol-util'
 import { Mat4 } from '../../../mol-math/linear-algebra'
-import { transformPositionArray/* , transformDirectionArray, getNormalMatrix */ } from '../../util';
+import { transformPositionArray,/* , transformDirectionArray, getNormalMatrix */
+GroupMapping,
+createGroupMapping} from '../../util';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
@@ -14,7 +16,7 @@ import { createSizes } from '../size-data';
 import { TransformData } from '../transform-data';
 import { LocationIterator } from '../../util/location-iterator';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { calculateBoundingSphere } from '../../../mol-gl/renderable/util';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Theme } from '../../../mol-theme/theme';
 import { PointsValues } from '../../../mol-gl/renderable/points';
@@ -23,37 +25,88 @@ import { Color } from '../../../mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
 
 /** Point cloud */
 export interface Points {
     readonly kind: 'points',
+
     /** Number of vertices in the point cloud */
     pointCount: number,
+
     /** Center buffer as array of xyz values wrapped in a value cell */
     readonly centerBuffer: ValueCell<Float32Array>,
     /** Group buffer as array of group ids for each vertex wrapped in a value cell */
     readonly groupBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the points */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to point indices */
+    readonly groupMapping: GroupMapping
 }
 
 export namespace Points {
+    export function create(centers: Float32Array, groups: Float32Array, pointCount: number, points?: Points): Points {
+        return points ?
+            update(centers, groups, pointCount, points) :
+            fromArrays(centers, groups, pointCount)
+    }
+
     export function createEmpty(points?: Points): Points {
         const cb = points ? points.centerBuffer.ref.value : new Float32Array(0)
         const gb = points ? points.groupBuffer.ref.value : new Float32Array(0)
-        return {
-            kind: 'points',
-            pointCount: 0,
-            centerBuffer: points ? ValueCell.update(points.centerBuffer, cb) : ValueCell.create(cb),
-            groupBuffer: points ? ValueCell.update(points.groupBuffer, gb) : ValueCell.create(gb),
+        return create(cb, gb, 0, points)
+    }
+
+    function hashCode(points: Points) {
+        return hashFnv32a([
+            points.pointCount, points.centerBuffer.ref.version, points.groupBuffer.ref.version,
+        ])
+    }
+
+    function fromArrays(centers: Float32Array, groups: Float32Array, pointCount: number): Points {
+
+        const boundingSphere = Sphere3D()
+        let groupMapping: GroupMapping
+
+        let currentHash = -1
+        let currentGroup = -1
+
+        const points = {
+            kind: 'points' as const,
+            pointCount,
+            centerBuffer: ValueCell.create(centers),
+            groupBuffer: ValueCell.create(groups),
+            get boundingSphere() {
+                const newHash = hashCode(points)
+                if (newHash !== currentHash) {
+                    const b = calculateInvariantBoundingSphere(points.centerBuffer.ref.value, points.pointCount, 1)
+                    Sphere3D.copy(boundingSphere, b)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
+            get groupMapping() {
+                if (points.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(points.groupBuffer.ref.value, points.pointCount)
+                    currentGroup = points.groupBuffer.ref.version
+                }
+                return groupMapping
+            }
         }
+        return points
     }
 
-    export function transformImmediate(points: Points, t: Mat4) {
-        transformRangeImmediate(points, t, 0, points.pointCount)
+    function update(centers: Float32Array, groups: Float32Array, pointCount: number, points: Points) {
+        points.pointCount = pointCount
+        ValueCell.update(points.centerBuffer, centers)
+        ValueCell.update(points.groupBuffer, groups)
+        return points
     }
 
-    export function transformRangeImmediate(points: Points, t: Mat4, offset: number, count: number) {
+    export function transform(points: Points, t: Mat4) {
         const c = points.centerBuffer.ref.value
-        transformPositionArray(t, c, offset, count)
+        transformPositionArray(t, c, 0, points.pointCount)
         ValueCell.update(points.centerBuffer, c);
     }
 
@@ -89,10 +142,8 @@ export namespace Points {
 
         const counts = { drawCount: points.pointCount, groupCount, instanceCount }
 
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            points.centerBuffer.ref.value, points.pointCount,
-            transform.aTransform.ref.value, transform.instanceCount.ref.value
-        )
+        const invariantBoundingSphere = Sphere3D.clone(points.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         return {
             aPosition: points.centerBuffer,
@@ -129,10 +180,9 @@ export namespace Points {
     }
 
     function updateBoundingSphere(values: PointsValues, points: Points) {
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            values.aPosition.ref.value, points.pointCount,
-            values.aTransform.ref.value, values.instanceCount.ref.value
-        )
+        const invariantBoundingSphere = Sphere3D.clone(points.boundingSphere)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }

+ 2 - 10
src/mol-geo/geometry/spheres/spheres-builder.ts

@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from '../../../mol-util/value-cell'
 import { ChunkedArray } from '../../../mol-data/util';
 import { Spheres } from './spheres';
 
@@ -50,14 +49,7 @@ export namespace SpheresBuilder {
                 const mb = ChunkedArray.compact(mappings, true) as Float32Array
                 const ib = ChunkedArray.compact(indices, true) as Uint32Array
                 const gb = ChunkedArray.compact(groups, true) as Float32Array
-                return {
-                    kind: 'spheres',
-                    sphereCount: centers.elementCount / 4,
-                    centerBuffer: spheres ? ValueCell.update(spheres.centerBuffer, cb) : ValueCell.create(cb),
-                    mappingBuffer: spheres ? ValueCell.update(spheres.mappingBuffer, mb) : ValueCell.create(mb),
-                    indexBuffer: spheres ? ValueCell.update(spheres.indexBuffer, ib) : ValueCell.create(ib),
-                    groupBuffer: spheres ? ValueCell.update(spheres.groupBuffer, gb) : ValueCell.create(gb),
-                }
+                return Spheres.create(cb, mb, ib, gb, centers.elementCount / 4, spheres)
             }
         }
     }

+ 72 - 18
src/mol-geo/geometry/spheres/spheres.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,15 +13,16 @@ import { Theme } from '../../../mol-theme/theme';
 import { SpheresValues } from '../../../mol-gl/renderable/spheres';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
-import { calculateBoundingSphere } from '../../../mol-gl/renderable/util';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { createSizes, getMaxSize } from '../size-data';
 import { Color } from '../../../mol-util/color';
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
+import { GroupMapping, createGroupMapping } from '../../util';
 
-/** Spheres */
 export interface Spheres {
     readonly kind: 'spheres',
 
@@ -36,22 +37,78 @@ export interface Spheres {
     readonly indexBuffer: ValueCell<Uint32Array>,
     /** Group buffer as array of group ids for each vertex wrapped in a value cell */
     readonly groupBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the spheres */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to sphere indices */
+    readonly groupMapping: GroupMapping
 }
 
 export namespace Spheres {
+    export function create(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number, spheres?: Spheres): Spheres {
+        return spheres ?
+            update(centers, mappings, indices, groups, sphereCount, spheres) :
+            fromArrays(centers, mappings, indices, groups, sphereCount)
+    }
+
     export function createEmpty(spheres?: Spheres): Spheres {
         const cb = spheres ? spheres.centerBuffer.ref.value : new Float32Array(0)
         const mb = spheres ? spheres.mappingBuffer.ref.value : new Float32Array(0)
         const ib = spheres ? spheres.indexBuffer.ref.value : new Uint32Array(0)
         const gb = spheres ? spheres.groupBuffer.ref.value : new Float32Array(0)
-        return {
-            kind: 'spheres',
-            sphereCount: 0,
-            centerBuffer: spheres ? ValueCell.update(spheres.centerBuffer, cb) : ValueCell.create(cb),
-            mappingBuffer: spheres ? ValueCell.update(spheres.mappingBuffer, mb) : ValueCell.create(mb),
-            indexBuffer: spheres ? ValueCell.update(spheres.indexBuffer, ib) : ValueCell.create(ib),
-            groupBuffer: spheres ? ValueCell.update(spheres.groupBuffer, gb) : ValueCell.create(gb)
+        return create(cb, mb, ib, gb, 0, spheres)
+    }
+
+    function hashCode(spheres: Spheres) {
+        return hashFnv32a([
+            spheres.sphereCount,
+            spheres.centerBuffer.ref.version, spheres.mappingBuffer.ref.version,
+            spheres.indexBuffer.ref.version, spheres.groupBuffer.ref.version
+        ])
+    }
+
+    function fromArrays(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number): Spheres {
+
+        const boundingSphere = Sphere3D()
+        let groupMapping: GroupMapping
+
+        let currentHash = -1
+        let currentGroup = -1
+
+        const spheres = {
+            kind: 'spheres' as const,
+            sphereCount,
+            centerBuffer: ValueCell.create(centers),
+            mappingBuffer: ValueCell.create(mappings),
+            indexBuffer: ValueCell.create(indices),
+            groupBuffer: ValueCell.create(groups),
+            get boundingSphere() {
+                const newHash = hashCode(spheres)
+                if (newHash !== currentHash) {
+                    const b = calculateInvariantBoundingSphere(spheres.centerBuffer.ref.value, spheres.sphereCount * 4, 4)
+                    Sphere3D.copy(boundingSphere, b)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
+            get groupMapping() {
+                if (spheres.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(spheres.groupBuffer.ref.value, spheres.sphereCount, 4)
+                    currentGroup = spheres.groupBuffer.ref.version
+                }
+                return groupMapping
+            }
         }
+        return spheres
+    }
+
+    function update(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number, spheres: Spheres) {
+        spheres.sphereCount = sphereCount
+        ValueCell.update(spheres.centerBuffer, centers)
+        ValueCell.update(spheres.mappingBuffer, mappings)
+        ValueCell.update(spheres.indexBuffer, indices)
+        ValueCell.update(spheres.groupBuffer, groups)
+        return spheres
     }
 
     export const Params = {
@@ -88,10 +145,8 @@ export namespace Spheres {
         const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount }
 
         const padding = getMaxSize(size)
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            spheres.centerBuffer.ref.value, spheres.sphereCount * 4,
-            transform.aTransform.ref.value, instanceCount, padding, 4
-        )
+        const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), spheres.boundingSphere, padding)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         return {
             aPosition: spheres.centerBuffer,
@@ -131,10 +186,9 @@ export namespace Spheres {
 
     function updateBoundingSphere(values: SpheresValues, spheres: Spheres) {
         const padding = getMaxSize(values)
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            values.aPosition.ref.value, spheres.sphereCount * 4,
-            values.aTransform.ref.value, values.instanceCount.ref.value, padding, 4
-        )
+        const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), spheres.boundingSphere, padding)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }

+ 5 - 5
src/mol-geo/geometry/text/font-atlas.ts

@@ -24,11 +24,11 @@ export type FontVariant = 'normal' | 'small-caps'
 export type FontWeight = 'normal' | 'bold'
 
 export const FontAtlasParams = {
-  fontFamily: PD.Select('sans-serif', [['sans-serif', 'Sans Serif'], ['monospace', 'Monospace'], ['serif', 'Serif'], ['cursive', 'Cursive']] as [FontFamily, string][]),
-  fontQuality: PD.Select(3, [[0, 'lower'], [1, 'low'], [2, 'medium'], [3, 'high'], [4, 'higher']]),
-  fontStyle: PD.Select('normal', [['normal', 'Normal'], ['italic', 'Italic'], ['oblique', 'Oblique']] as [FontStyle, string][]),
-  fontVariant: PD.Select('normal', [['normal', 'Normal'], ['small-caps', 'Small Caps']] as [FontVariant, string][]),
-  fontWeight: PD.Select('normal', [['normal', 'Normal'], ['bold', 'Bold']] as [FontWeight, string][]),
+    fontFamily: PD.Select('sans-serif', [['sans-serif', 'Sans Serif'], ['monospace', 'Monospace'], ['serif', 'Serif'], ['cursive', 'Cursive']] as [FontFamily, string][]),
+    fontQuality: PD.Select(3, [[0, 'lower'], [1, 'low'], [2, 'medium'], [3, 'high'], [4, 'higher']]),
+    fontStyle: PD.Select('normal', [['normal', 'Normal'], ['italic', 'Italic'], ['oblique', 'Oblique']] as [FontStyle, string][]),
+    fontVariant: PD.Select('normal', [['normal', 'Normal'], ['small-caps', 'Small Caps']] as [FontVariant, string][]),
+    fontWeight: PD.Select('normal', [['normal', 'Normal'], ['bold', 'Bold']] as [FontWeight, string][]),
 }
 export type FontAtlasParams = typeof FontAtlasParams
 export type FontAtlasProps = PD.Values<FontAtlasParams>

+ 2 - 13
src/mol-geo/geometry/text/text-builder.ts

@@ -1,11 +1,10 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { ValueCell } from '../../../mol-util/value-cell'
 import { ChunkedArray } from '../../../mol-data/util';
 import { Text } from './text';
 import { getFontAtlas } from './font-atlas';
@@ -290,17 +289,7 @@ export namespace TextBuilder {
                 const ib = ChunkedArray.compact(indices, true) as Uint32Array
                 const gb = ChunkedArray.compact(groups, true) as Float32Array
                 const tb = ChunkedArray.compact(tcoords, true) as Float32Array
-                return {
-                    kind: 'text',
-                    charCount: indices.elementCount / 2,
-                    fontTexture: text ? ValueCell.update(text.fontTexture, ft) : ValueCell.create(ft),
-                    centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
-                    mappingBuffer: text ? ValueCell.update(text.mappingBuffer, mb) : ValueCell.create(mb),
-                    depthBuffer: text ? ValueCell.update(text.depthBuffer, db) : ValueCell.create(db),
-                    indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
-                    groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
-                    tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb),
-                }
+                return Text.create(ft, cb,mb, db, ib, gb, tb, indices.elementCount / 2, text)
             }
         }
     }

+ 81 - 21
src/mol-geo/geometry/text/text.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -15,7 +15,7 @@ import { createSizes, getMaxSize } from '../size-data';
 import { createMarkers } from '../marker-data';
 import { ColorNames } from '../../../mol-util/color/names';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { calculateBoundingSphere, TextureImage, createTextureImage } from '../../../mol-gl/renderable/util';
+import { TextureImage, createTextureImage, calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { TextValues } from '../../../mol-gl/renderable/text';
 import { Color } from '../../../mol-util/color';
 import { Vec3 } from '../../../mol-math/linear-algebra';
@@ -26,6 +26,8 @@ import { createRenderObject as _createRenderObject } from '../../../mol-gl/rende
 import { BaseGeometry } from '../base';
 import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
+import { GroupMapping, createGroupMapping } from '../../util';
 
 type TextAttachment = (
     'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -38,7 +40,8 @@ export interface Text {
     readonly kind: 'text',
 
     /** Number of characters in the text */
-    readonly charCount: number,
+    charCount: number,
+
     /** Font Atlas */
     readonly fontTexture: ValueCell<TextureImage<Uint8Array>>,
 
@@ -54,9 +57,20 @@ export interface Text {
     readonly groupBuffer: ValueCell<Float32Array>,
     /** Texture coordinates buffer as array of uv values wrapped in a value cell */
     readonly tcoordBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the text */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to text indices */
+    readonly groupMapping: GroupMapping
 }
 
 export namespace Text {
+    export function create(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number, text?: Text): Text {
+        return text ?
+            update(fontTexture, centers, mappings, depths, indices, groups, tcoords, charCount, text) :
+            fromData(fontTexture, centers, mappings, depths, indices, groups, tcoords, charCount)
+    }
+
     export function createEmpty(text?: Text): Text {
         const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1, Uint8Array)
         const cb = text ? text.centerBuffer.ref.value : new Float32Array(0)
@@ -65,17 +79,66 @@ export namespace Text {
         const ib = text ? text.indexBuffer.ref.value : new Uint32Array(0)
         const gb = text ? text.groupBuffer.ref.value : new Float32Array(0)
         const tb = text ? text.tcoordBuffer.ref.value : new Float32Array(0)
-        return {
-            kind: 'text',
-            charCount: 0,
-            fontTexture: text ? ValueCell.update(text.fontTexture, ft) : ValueCell.create(ft),
-            centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
-            mappingBuffer: text ? ValueCell.update(text.mappingBuffer, mb) : ValueCell.create(mb),
-            depthBuffer: text ? ValueCell.update(text.depthBuffer, db) : ValueCell.create(db),
-            indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
-            groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
-            tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb)
+        return create(ft, cb, mb, db, ib, gb, tb, 0, text)
+    }
+
+    function hashCode(text: Text) {
+        return hashFnv32a([
+            text.charCount, text.fontTexture.ref.version,
+            text.centerBuffer.ref.version, text.mappingBuffer.ref.version,
+            text.depthBuffer.ref.version, text.indexBuffer.ref.version,
+            text.groupBuffer.ref.version, text.tcoordBuffer.ref.version
+        ])
+    }
+
+    function fromData(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number): Text {
+
+        const boundingSphere = Sphere3D()
+        let groupMapping: GroupMapping
+
+        let currentHash = -1
+        let currentGroup = -1
+
+        const text = {
+            kind: 'text' as const,
+            charCount,
+            fontTexture: ValueCell.create(fontTexture),
+            centerBuffer: ValueCell.create(centers),
+            mappingBuffer: ValueCell.create(mappings),
+            depthBuffer: ValueCell.create(depths),
+            indexBuffer: ValueCell.create(indices),
+            groupBuffer: ValueCell.create(groups),
+            tcoordBuffer: ValueCell.create(tcoords),
+            get boundingSphere() {
+                const newHash = hashCode(text)
+                if (newHash !== currentHash) {
+                    const b = calculateInvariantBoundingSphere(text.centerBuffer.ref.value, text.charCount * 4, 4)
+                    Sphere3D.copy(boundingSphere, b)
+                    currentHash = newHash
+                }
+                return boundingSphere
+            },
+            get groupMapping() {
+                if (text.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(text.groupBuffer.ref.value, text.charCount, 4)
+                    currentGroup = text.groupBuffer.ref.version
+                }
+                return groupMapping
+            }
         }
+        return text
+    }
+
+    function update(fontTexture: TextureImage<Uint8Array>, centers: Float32Array, mappings: Float32Array, depths: Float32Array, indices: Uint32Array, groups: Float32Array, tcoords: Float32Array, charCount: number, text: Text) {
+        text.charCount = charCount
+        ValueCell.update(text.fontTexture, fontTexture)
+        ValueCell.update(text.centerBuffer, centers)
+        ValueCell.update(text.mappingBuffer, mappings)
+        ValueCell.update(text.depthBuffer, depths)
+        ValueCell.update(text.indexBuffer, indices)
+        ValueCell.update(text.groupBuffer, groups)
+        ValueCell.update(text.tcoordBuffer, tcoords)
+        return text
     }
 
     export const Params = {
@@ -130,10 +193,8 @@ export namespace Text {
         const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount }
 
         const padding = getPadding(text.mappingBuffer.ref.value, text.depthBuffer.ref.value, text.charCount, getMaxSize(size))
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            text.centerBuffer.ref.value, text.charCount * 4,
-            transform.aTransform.ref.value, instanceCount, padding
-        )
+        const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), text.boundingSphere, padding)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount)
 
         return {
             aPosition: text.centerBuffer,
@@ -194,10 +255,9 @@ export namespace Text {
 
     function updateBoundingSphere(values: TextValues, text: Text) {
         const padding = getPadding(values.aMapping.ref.value, values.aDepth.ref.value, text.charCount, getMaxSize(values))
-        const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
-            values.aPosition.ref.value, text.charCount * 4,
-            values.aTransform.ref.value, values.instanceCount.ref.value, padding
-        )
+        const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), text.boundingSphere, padding)
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
+
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }

+ 16 - 15
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -27,38 +27,39 @@ export interface TextureMesh {
     readonly kind: 'texture-mesh',
 
     /** Number of vertices in the texture-mesh */
-    readonly vertexCount: ValueCell<number>,
+    vertexCount: number,
     /** Number of groups in the texture-mesh */
-    readonly groupCount: ValueCell<number>,
+    groupCount: number,
 
     readonly geoTextureDim: ValueCell<Vec2>,
     /** texture has vertex positions in XYZ and group id in W */
     readonly vertexGroupTexture: ValueCell<Texture>,
     readonly normalTexture: ValueCell<Texture>,
 
-    readonly boundingSphere: ValueCell<Sphere3D>,
+    readonly boundingSphere: Sphere3D
 }
 
 export namespace TextureMesh {
     export function create(vertexCount: number, groupCount: number, vertexGroupTexture: Texture, normalTexture: Texture, boundingSphere: Sphere3D, textureMesh?: TextureMesh): TextureMesh {
-        const { width, height } = vertexGroupTexture
+        const width = vertexGroupTexture.getWidth()
+        const height = vertexGroupTexture.getHeight()
         if (textureMesh) {
-            ValueCell.update(textureMesh.vertexCount, vertexCount)
-            ValueCell.update(textureMesh.groupCount, groupCount)
+            textureMesh.vertexCount = vertexCount
+            textureMesh.groupCount = groupCount
             ValueCell.update(textureMesh.geoTextureDim, Vec2.set(textureMesh.geoTextureDim.ref.value, width, height))
             ValueCell.update(textureMesh.vertexGroupTexture, vertexGroupTexture)
             ValueCell.update(textureMesh.normalTexture, normalTexture)
-            ValueCell.update(textureMesh.boundingSphere, boundingSphere)
+            Sphere3D.copy(textureMesh.boundingSphere, boundingSphere)
             return textureMesh
         } else {
             return {
                 kind: 'texture-mesh',
-                vertexCount: ValueCell.create(vertexCount),
-                groupCount: ValueCell.create(groupCount),
+                vertexCount,
+                groupCount,
                 geoTextureDim: ValueCell.create(Vec2.create(width, height)),
                 vertexGroupTexture: ValueCell.create(vertexGroupTexture),
                 normalTexture: ValueCell.create(normalTexture),
-                boundingSphere: ValueCell.create(boundingSphere),
+                boundingSphere: Sphere3D.clone(boundingSphere),
             }
         }
     }
@@ -93,9 +94,9 @@ export namespace TextureMesh {
         const overpaint = createEmptyOverpaint()
         const transparency = createEmptyTransparency()
 
-        const counts = { drawCount: textureMesh.vertexCount.ref.value, groupCount, instanceCount }
+        const counts = { drawCount: textureMesh.vertexCount, groupCount, instanceCount }
 
-        const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere.ref.value, transform.aTransform.ref.value, transform.instanceCount.ref.value)
+        const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere, transform.aTransform.ref.value, transform.instanceCount.ref.value)
 
         return {
             uGeoTexDim: textureMesh.geoTextureDim,
@@ -103,9 +104,9 @@ export namespace TextureMesh {
             tNormal: textureMesh.normalTexture,
 
             // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup
-            aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount.ref.value))),
+            aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount))),
             boundingSphere: ValueCell.create(transformBoundingSphere),
-            invariantBoundingSphere: textureMesh.boundingSphere,
+            invariantBoundingSphere: ValueCell.create(Sphere3D.clone(textureMesh.boundingSphere)),
 
             ...color,
             ...marker,
@@ -141,7 +142,7 @@ export namespace TextureMesh {
     }
 
     function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {
-        const invariantBoundingSphere = textureMesh.boundingSphere.ref.value
+        const invariantBoundingSphere = textureMesh.boundingSphere
         const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value)
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)

+ 14 - 17
src/mol-geo/primitive/box.ts

@@ -61,23 +61,20 @@ export function PerforatedBox() {
 let boxCage: Cage
 export function BoxCage() {
     if (!boxCage) {
-        boxCage = createCage(
-            [
-                 0.5,  0.5, -0.5, // bottom
-                -0.5,  0.5, -0.5,
-                -0.5, -0.5, -0.5,
-                 0.5, -0.5, -0.5,
-                 0.5,  0.5, 0.5,  // top
-                -0.5,  0.5, 0.5,
-                -0.5, -0.5, 0.5,
-                 0.5, -0.5, 0.5
-            ],
-            [
-                0, 4,  1, 5,  2, 6,  3, 7, // sides
-                0, 1,  1, 2,  2, 3,  3, 0,  // bottom base
-                4, 5,  5, 6,  6, 7,  7, 4   // top base
-            ]
-        )
+        boxCage = createCage([
+            0.5,  0.5, -0.5, // bottom
+            -0.5,  0.5, -0.5,
+            -0.5, -0.5, -0.5,
+            0.5, -0.5, -0.5,
+            0.5,  0.5, 0.5,  // top
+            -0.5,  0.5, 0.5,
+            -0.5, -0.5, 0.5,
+            0.5, -0.5, 0.5
+        ], [
+            0, 4,  1, 5,  2, 6,  3, 7, // sides
+            0, 1,  1, 2,  2, 3,  3, 0,  // bottom base
+            4, 5,  5, 6,  6, 7,  7, 4   // top base
+        ])
     }
     return boxCage
 }

+ 1 - 1
src/mol-geo/primitive/cage.ts

@@ -16,7 +16,7 @@ export function createCage(vertices: ArrayLike<number>, edges: ArrayLike<number>
     return { vertices, edges }
 }
 
-export function copyCage(cage: Cage): Cage {
+export function cloneCage(cage: Cage): Cage {
     return {
         vertices: new Float32Array(cage.vertices),
         edges: new Uint32Array(cage.edges)

+ 16 - 16
src/mol-geo/primitive/dodecahedron.ts

@@ -14,46 +14,46 @@ const b = 1 / t;
 const c = 2 - t;
 
 export const dodecahedronVertices: ReadonlyArray<number> = [
-     c, 0, a,    -c, 0, a,    -b, b, b,    0, a, c,     b, b, b,
-     b, -b, b,    0, -a, c,   -b, -b, b,   c, 0, -a,   -c, 0, -a,
+    c, 0, a,    -c, 0, a,    -b, b, b,    0, a, c,     b, b, b,
+    b, -b, b,    0, -a, c,   -b, -b, b,   c, 0, -a,   -c, 0, -a,
     -b, -b, -b,   0, -a, -c,   b, -b, -b,  b,  b, -b,   0, a, -c,
     -b, b, -b,    a, c, 0,    -a, c, 0,   -a, -c, 0,    a, -c, 0
 ];
 
 /** indices of pentagonal faces, groups of five  */
 export const dodecahedronFaces: ReadonlyArray<number> = [
-     4, 3, 2, 1, 0,
-     7, 6, 5, 0, 1,
+    4, 3, 2, 1, 0,
+    7, 6, 5, 0, 1,
     12, 11, 10, 9, 8,
     15, 14, 13, 8, 9,
     14, 3, 4, 16, 13,
-     3, 14, 15, 17, 2,
+    3, 14, 15, 17, 2,
     11, 6, 7, 18, 10,
-     6, 11, 12, 19, 5,
-     4, 0, 5, 19, 16,
+    6, 11, 12, 19, 5,
+    4, 0, 5, 19, 16,
     12, 8, 13, 16, 19,
     15, 9, 10, 18, 17,
-     7, 1, 2, 17, 18
+    7, 1, 2, 17, 18
 ];
 
 const dodecahedronIndices: ReadonlyArray<number> = [  // pentagonal faces
-     4, 3, 2,     2, 1, 0,     4, 2, 0,    // 4, 3, 2, 1, 0
-     7, 6, 5,     5, 0, 1,     7, 5, 1,    // 7, 6, 5, 0, 1
+    4, 3, 2,     2, 1, 0,     4, 2, 0,    // 4, 3, 2, 1, 0
+    7, 6, 5,     5, 0, 1,     7, 5, 1,    // 7, 6, 5, 0, 1
     12, 11, 10,  10, 9, 8,    12, 10, 8,   // 12, 11, 10, 9, 8
     15, 14, 13,  13, 8, 9,    15, 13, 9,   // 15, 14, 13, 8, 9
     14, 3, 4,     4, 16, 13,  14, 4, 13,   // 14, 3, 4, 16, 13
-     3, 14, 15,   15, 17, 2,   3, 15, 2,   // 3, 14, 15, 17, 2
+    3, 14, 15,   15, 17, 2,   3, 15, 2,   // 3, 14, 15, 17, 2
     11, 6, 7,     7, 18, 10,  11, 7, 10,   // 11, 6, 7, 18, 10
-     6, 11, 12,  12, 19, 5,    6, 12, 5,   // 6, 11, 12, 19, 5
-     4, 0, 5,     5, 19, 16,   4, 5, 16,   // 4, 0, 5, 19, 16
+    6, 11, 12,  12, 19, 5,    6, 12, 5,   // 6, 11, 12, 19, 5
+    4, 0, 5,     5, 19, 16,   4, 5, 16,   // 4, 0, 5, 19, 16
     12, 8, 13,   13, 16, 19,  12, 13, 19,  // 12, 8, 13, 16, 19
     15, 9, 10,   10, 18, 17,  15, 10, 17,  // 15, 9, 10, 18, 17
-     7, 1, 2,     2, 17, 18,   7, 2, 18,   // 7, 1, 2, 17, 18
+    7, 1, 2,     2, 17, 18,   7, 2, 18,   // 7, 1, 2, 17, 18
 ];
 
 const dodecahedronEdges: ReadonlyArray<number> = [
-     0, 1,   0, 4,    0, 5,    1, 2,    1, 7,    2, 3,    2, 17,   3, 4,    3, 14,   4, 16,
-     5, 6,   5, 19,   6, 7,    6, 11,   7, 18,   8, 9,    8, 12,   8, 13,   9, 10,   9, 15,
+    0, 1,   0, 4,    0, 5,    1, 2,    1, 7,    2, 3,    2, 17,   3, 4,    3, 14,   4, 16,
+    5, 6,   5, 19,   6, 7,    6, 11,   7, 18,   8, 9,    8, 12,   8, 13,   9, 10,   9, 15,
     10, 11, 10, 18,  11, 12,  12, 19,  13, 14,  13, 16,  14, 15,  15, 17,  16, 19,  17, 18,
 ]
 

+ 2 - 2
src/mol-geo/primitive/icosahedron.ts

@@ -11,8 +11,8 @@ const t = (1 + Math.sqrt(5)) / 2;
 
 const icosahedronVertices: ReadonlyArray<number> = [
     -1, t, 0,   1, t, 0,  -1, -t, 0,   1, -t, 0,
-     0, -1, t,  0, 1, t,   0, -1, -t,  0, 1, -t,
-     t, 0, -1,  t, 0, 1,  -t, 0, -1,  -t, 0, 1
+    0, -1, t,  0, 1, t,   0, -1, -t,  0, 1, -t,
+    t, 0, -1,  t, 0, 1,  -t, 0, -1,  -t, 0, 1
 ];
 
 const icosahedronIndices: ReadonlyArray<number> = [

+ 8 - 9
src/mol-geo/primitive/polyhedron.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -28,7 +28,7 @@ export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<num
     appplyRadius(vertices, radius);
 
     const normals = new Float32Array(vertices.length);
-    computeIndexedVertexNormals(vertices, indices, normals)
+    computeIndexedVertexNormals(vertices, indices, normals, vertices.length / 3, indices.length / 3)
 
     return {
         vertices: new Float32Array(vertices),
@@ -39,9 +39,9 @@ export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<num
     // helper functions
 
     function subdivide(detail: number) {
-        const a = Vec3.zero()
-        const b = Vec3.zero()
-        const c = Vec3.zero()
+        const a = Vec3()
+        const b = Vec3()
+        const c = Vec3()
 
         // iterate over all faces and apply a subdivison with the given detail value
         for (let i = 0; i < _indices.length; i += 3) {
@@ -66,10 +66,10 @@ export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<num
         for (let i = 0; i <= cols; ++i) {
             v[i] = []
 
-            const aj = Vec3.zero()
+            const aj = Vec3()
             Vec3.lerp(aj, a, c, i / cols)
 
-            const bj = Vec3.zero()
+            const bj = Vec3()
             Vec3.lerp(bj, b, c, i / cols)
 
             const rows = cols - i
@@ -77,7 +77,7 @@ export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<num
                 if (j === 0 && i === cols) {
                     v[i][j] = aj
                 } else {
-                    const abj = Vec3.zero()
+                    const abj = Vec3()
                     Vec3.lerp(abj, aj, bj, j / rows)
 
                     v[i][j] = abj
@@ -90,7 +90,6 @@ export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<num
             for (let j = 0; j < 2 * (cols - i) - 1; ++j) {
                 const k = Math.floor(j / 2)
                 if (j % 2 === 0) {
-                    builder.add
                     builder.add(v[i][k + 1], v[i + 1][k], v[i][k])
                 } else {
                     builder.add(v[i][k + 1], v[i + 1][k + 1], v[i + 1][k])

+ 3 - 7
src/mol-geo/primitive/tetrahedron.ts

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

+ 99 - 70
src/mol-geo/util.ts

@@ -1,23 +1,24 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Vec3, Mat4, Mat3 } from '../mol-math/linear-algebra'
 import { NumberArray } from '../mol-util/type-helpers';
+import { arrayMax } from '../mol-util/array';
 
-export function normalizeVec3Array<T extends NumberArray> (a: T) {
-    const n = a.length
-    for (let i = 0; i < n; i += 3) {
-        const x = a[ i ]
-        const y = a[ i + 1 ]
-        const z = a[ i + 2 ]
+export function normalizeVec3Array<T extends NumberArray> (a: T, count: number) {
+    for (let i = 0, il = count * 3; i < il; i += 3) {
+        const x = a[i]
+        const y = a[i + 1]
+        const z = a[i + 2]
         const s = 1 / Math.sqrt(x * x + y * y + z * z)
-        a[ i ] = x * s
-        a[ i + 1 ] = y * s
-        a[ i + 2 ] = z * s
+        a[i] = x * s
+        a[i + 1] = y * s
+        a[i + 2] = z * s
     }
+    return a
 }
 
 const tmpV3 = Vec3.zero()
@@ -38,38 +39,33 @@ export function transformDirectionArray (n: Mat3, array: NumberArray, offset: nu
     }
 }
 
-export function setArrayZero(array: NumberArray) {
-    const n = array.length
-    for (let i = 0; i < n; ++i) array[i] = 0
-}
-
-/** iterate over the entire buffer and apply the radius to each vertex */
+/** iterate over the entire array and apply the radius to each vertex */
 export function appplyRadius(vertices: NumberArray, radius: number) {
-    const v = Vec3.zero()
-    const n = vertices.length
-    for (let i = 0; i < n; i += 3) {
-        Vec3.fromArray(v, vertices, i)
-        Vec3.normalize(v, v)
-        Vec3.scale(v, v, radius)
-        Vec3.toArray(v, vertices, i)
+    for (let i = 0, il = vertices.length; i < il; i += 3) {
+        Vec3.fromArray(tmpV3, vertices, i)
+        Vec3.normalize(tmpV3, tmpV3)
+        Vec3.scale(tmpV3, tmpV3, radius)
+        Vec3.toArray(tmpV3, vertices, i)
     }
 }
 
+const a = Vec3()
+const b = Vec3()
+const c = Vec3()
+const cb = Vec3()
+const ab = Vec3()
+
 /**
- * indexed vertex normals weighted by triangle areas http://www.iquilezles.org/www/articles/normals/normals.htm
- * normal array must contain only zeros
+ * indexed vertex normals weighted by triangle areas
+ *      http://www.iquilezles.org/www/articles/normals/normals.htm
+ * - normals array must contain only zeros
  */
-export function computeIndexedVertexNormals<T extends NumberArray> (vertices: NumberArray, indices: NumberArray, normals: T) {
-    const a = Vec3.zero()
-    const b = Vec3.zero()
-    const c = Vec3.zero()
-    const cb = Vec3.zero()
-    const ab = Vec3.zero()
-
-    for (let i = 0, il = indices.length; i < il; i += 3) {
-        const ai = indices[ i ] * 3
-        const bi = indices[ i + 1 ] * 3
-        const ci = indices[ i + 2 ] * 3
+export function computeIndexedVertexNormals<T extends NumberArray> (vertices: NumberArray, indices: NumberArray, normals: T, vertexCount: number, triangleCount: number) {
+
+    for (let i = 0, il = triangleCount * 3; i < il; i += 3) {
+        const ai = indices[i] * 3
+        const bi = indices[i + 1] * 3
+        const ci = indices[i + 2] * 3
 
         Vec3.fromArray(a, vertices, ai)
         Vec3.fromArray(b, vertices, bi)
@@ -79,34 +75,28 @@ export function computeIndexedVertexNormals<T extends NumberArray> (vertices: Nu
         Vec3.sub(ab, a, b)
         Vec3.cross(cb, cb, ab)
 
-        normals[ ai ] += cb[ 0 ]
-        normals[ ai + 1 ] += cb[ 1 ]
-        normals[ ai + 2 ] += cb[ 2 ]
+        normals[ai] += cb[0]
+        normals[ai + 1] += cb[1]
+        normals[ai + 2] += cb[2]
 
-        normals[ bi ] += cb[ 0 ]
-        normals[ bi + 1 ] += cb[ 1 ]
-        normals[ bi + 2 ] += cb[ 2 ]
+        normals[bi] += cb[0]
+        normals[bi + 1] += cb[1]
+        normals[bi + 2] += cb[2]
 
-        normals[ ci ] += cb[ 0 ]
-        normals[ ci + 1 ] += cb[ 1 ]
-        normals[ ci + 2 ] += cb[ 2 ]
+        normals[ci] += cb[0]
+        normals[ci + 1] += cb[1]
+        normals[ci + 2] += cb[2]
     }
 
-    normalizeVec3Array(normals)
-    return normals
+    return normalizeVec3Array(normals, vertexCount)
 }
 
-/** vertex normals for unindexed triangle soup, normal array must contain only zeros */
-export function computeVertexNormals<T extends NumberArray> (vertices: NumberArray, normals: T) {
-    setArrayZero(normals)
-
-    const a = Vec3.zero()
-    const b = Vec3.zero()
-    const c = Vec3.zero()
-    const cb = Vec3.zero()
-    const ab = Vec3.zero()
-
-     for (let i = 0, il = vertices.length; i < il; i += 9) {
+/**
+ * vertex normals for unindexed triangle soup
+ * - normals array must contain only zeros
+ */
+export function computeVertexNormals<T extends NumberArray> (vertices: NumberArray, normals: T, vertexCount: number) {
+    for (let i = 0, il = vertexCount * 3; i < il; i += 9) {
         Vec3.fromArray(a, vertices, i)
         Vec3.fromArray(b, vertices, i + 3)
         Vec3.fromArray(c, vertices, i + 6)
@@ -115,19 +105,58 @@ export function computeVertexNormals<T extends NumberArray> (vertices: NumberArr
         Vec3.sub(ab, a, b)
         Vec3.cross(cb, cb, ab)
 
-        normals[ i ] = cb[ 0 ]
-        normals[ i + 1 ] = cb[ 1 ]
-        normals[ i + 2 ] = cb[ 2 ]
+        normals[i] = cb[0]
+        normals[i + 1] = cb[1]
+        normals[i + 2] = cb[2]
+
+        normals[i + 3] = cb[0]
+        normals[i + 4] = cb[1]
+        normals[i + 5] = cb[2]
+
+        normals[i + 6] = cb[0]
+        normals[i + 7] = cb[1]
+        normals[i + 8] = cb[2]
+    }
+
+    return normalizeVec3Array(normals, vertexCount)
+}
+
+/**
+ * Maps groups to data, range for group i is offsets[i] to offsets[i + 1]
+ */
+export type GroupMapping = {
+    /** data indices */
+    readonly indices: ArrayLike<number>
+    /** range for group i is offsets[i] to offsets[i + 1] */
+    readonly offsets: ArrayLike<number>
+}
+
+/**
+ * The `step` parameter allows to skip over repeated values in `groups`
+ */
+export function createGroupMapping(groups: ArrayLike<number>, dataCount: number, step = 1): GroupMapping {
+    const maxId = arrayMax(groups)
+
+    const offsets = new Int32Array(maxId + 2)
+    const bucketFill = new Int32Array(dataCount)
+    const bucketSizes = new Int32Array(dataCount)
 
-        normals[ i + 3 ] = cb[ 0 ]
-        normals[ i + 4 ] = cb[ 1 ]
-        normals[ i + 5 ] = cb[ 2 ]
+    for (let i = 0, il = dataCount * step; i < il; i += step) ++bucketSizes[groups[i]]
 
-        normals[ i + 6 ] = cb[ 0 ]
-        normals[ i + 7 ] = cb[ 1 ]
-        normals[ i + 8 ] = cb[ 2 ]
+    let offset = 0
+    for (let i = 0; i < dataCount; i++) {
+        offsets[i] = offset
+        offset += bucketSizes[i]
+    }
+    offsets[dataCount] = offset
+
+    const indices = new Int32Array(offset)
+    for (let i = 0, il = dataCount * step; i < il; i += step) {
+        const g = groups[i]
+        const og = offsets[g] + bucketFill[g]
+        indices[og] = i
+        ++bucketFill[g]
     }
 
-    normalizeVec3Array(normals)
-    return normals
-}
+    return { indices, offsets }
+}

+ 4 - 14
src/mol-geo/util/marching-cubes/builder.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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>
@@ -7,13 +7,13 @@
  */
 
 import { ChunkedArray } from '../../../mol-data/util';
-import { ValueCell, noop } from '../../../mol-util';
+import { noop } from '../../../mol-util';
 import { Mesh } from '../../geometry/mesh/mesh';
 import { AllowedContours } from './tables';
 import { LinesBuilder } from '../../geometry/lines/lines-builder';
 import { Lines } from '../../geometry/lines/lines';
 
- export interface MarchinCubesBuilder<T> {
+export interface MarchinCubesBuilder<T> {
     addVertex(x: number, y: number, z: number): number
     addNormal(x: number, y: number, z: number): void
     addGroup(group: number): void
@@ -52,17 +52,7 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M
             const nb = ChunkedArray.compact(normals, true) as Float32Array;
             const ib = ChunkedArray.compact(indices, true) as Uint32Array;
             const gb = ChunkedArray.compact(groups, true) as Float32Array;
-
-            return {
-                kind: 'mesh',
-                vertexCount,
-                triangleCount,
-                vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
-                groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
-                indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
-                normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
-                normalsComputed: true
-            }
+            return Mesh.create(vb, ib, nb, gb, vertexCount, triangleCount, mesh)
         }
     }
 }

+ 20 - 22
src/mol-gl/_spec/renderer.spec.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -101,11 +101,11 @@ describe('renderer', () => {
         expect(ctx.gl.canvas.width).toBe(32)
         expect(ctx.gl.canvas.height).toBe(32)
 
-        expect(ctx.stats.bufferCount).toBe(0);
-        expect(ctx.stats.textureCount).toBe(0);
-        expect(ctx.stats.vaoCount).toBe(0);
-        expect(ctx.programCache.count).toBe(0);
-        expect(ctx.shaderCache.count).toBe(0);
+        expect(ctx.stats.resourceCounts.attribute).toBe(0);
+        expect(ctx.stats.resourceCounts.texture).toBe(0);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(0);
+        expect(ctx.stats.resourceCounts.program).toBe(0);
+        expect(ctx.stats.resourceCounts.shader).toBe(0);
 
         renderer.setViewport(0, 0, 64, 48)
         expect(ctx.gl.getParameter(ctx.gl.VIEWPORT)[2]).toBe(64)
@@ -122,24 +122,22 @@ describe('renderer', () => {
 
         scene.add(points)
         await scene.commit().run()
-        expect(ctx.stats.bufferCount).toBe(4);
-        expect(ctx.stats.textureCount).toBe(5);
-        expect(ctx.stats.vaoCount).toBe(5);
-        expect(ctx.programCache.count).toBe(5);
-        expect(ctx.shaderCache.count).toBe(10);
+        expect(ctx.stats.resourceCounts.attribute).toBe(4);
+        expect(ctx.stats.resourceCounts.texture).toBe(5);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(5);
+        expect(ctx.stats.resourceCounts.program).toBe(5);
+        expect(ctx.stats.resourceCounts.shader).toBe(10);
 
         scene.remove(points)
         await scene.commit().run()
-        expect(ctx.stats.bufferCount).toBe(0);
-        expect(ctx.stats.textureCount).toBe(0);
-        expect(ctx.stats.vaoCount).toBe(0);
-        expect(ctx.programCache.count).toBe(5);
-        expect(ctx.shaderCache.count).toBe(10);
-
-        ctx.programCache.dispose()
-        expect(ctx.programCache.count).toBe(0);
-
-        ctx.shaderCache.clear()
-        expect(ctx.shaderCache.count).toBe(0);
+        expect(ctx.stats.resourceCounts.attribute).toBe(0);
+        expect(ctx.stats.resourceCounts.texture).toBe(0);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(0);
+        expect(ctx.stats.resourceCounts.program).toBe(5);
+        expect(ctx.stats.resourceCounts.shader).toBe(10);
+
+        ctx.resources.destroy()
+        expect(ctx.stats.resourceCounts.program).toBe(0);
+        expect(ctx.stats.resourceCounts.shader).toBe(0);
     })
 })

+ 9 - 9
src/mol-gl/compute/histogram-pyramid/reduction.ts

@@ -8,13 +8,13 @@ import { createComputeRenderable, ComputeRenderable } from '../../renderable'
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
-import { Texture, createTexture } from '../../../mol-gl/webgl/texture';
+import { Texture } from '../../../mol-gl/webgl/texture';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { QuadSchema, QuadValues } from '../util';
 import { Vec2 } from '../../../mol-math/linear-algebra';
 import { getHistopyramidSum } from './sum';
-import { Framebuffer, createFramebuffer } from '../../../mol-gl/webgl/framebuffer';
+import { Framebuffer } from '../../../mol-gl/webgl/framebuffer';
 import { isPowerOfTwo } from '../../../mol-math/misc';
 import quad_vert from '../../../mol-gl/shader/quad.vert'
 import reduction_frag from '../../../mol-gl/shader/histogram-pyramid/reduction.frag'
@@ -55,8 +55,8 @@ function getLevelTextureFramebuffer(ctx: WebGLContext, level: number) {
     let textureFramebuffer  = LevelTexturesFramebuffers[level]
     const size = Math.pow(2, level)
     if (textureFramebuffer === undefined) {
-        const texture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-        const framebuffer = createFramebuffer(ctx.gl, ctx.stats)
+        const texture = ctx.resources.texture('image-float32', 'rgba', 'float', 'nearest')
+        const framebuffer = ctx.resources.framebuffer()
         texture.attachFramebuffer(framebuffer, 0)
         textureFramebuffer = { texture, framebuffer }
         textureFramebuffer.texture.define(size, size)
@@ -85,22 +85,22 @@ export interface HistogramPyramid {
 }
 
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2): HistogramPyramid {
-    const { gl, framebufferCache } = ctx
+    const { gl, resources } = ctx
 
     // printTexture(ctx, inputTexture, 2)
-    if (inputTexture.width !== inputTexture.height || !isPowerOfTwo(inputTexture.width)) {
+    if (inputTexture.getWidth() !== inputTexture.getHeight() || !isPowerOfTwo(inputTexture.getWidth())) {
         throw new Error('inputTexture must be of square power-of-two size')
     }
 
     // This part set the levels
-    const levels = Math.ceil(Math.log(inputTexture.width) / Math.log(2))
+    const levels = Math.ceil(Math.log(inputTexture.getWidth()) / Math.log(2))
     const maxSize = Math.pow(2, levels)
     // console.log('levels', levels, 'maxSize', maxSize)
 
-    const pyramidTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    const pyramidTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest')
     pyramidTexture.define(maxSize, maxSize)
 
-    const framebuffer = framebufferCache.get('reduction').value
+    const framebuffer = resources.framebuffer()
     pyramidTexture.attachFramebuffer(framebuffer, 0)
     gl.clear(gl.COLOR_BUFFER_BIT)
 

+ 4 - 7
src/mol-gl/compute/histogram-pyramid/sum.ts

@@ -8,7 +8,7 @@ import { createComputeRenderable, ComputeRenderable } from '../../renderable'
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec } from '../../renderable/schema';
-import { Texture, createTexture } from '../../../mol-gl/webgl/texture';
+import { Texture } from '../../../mol-gl/webgl/texture';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { decodeFloatRGB } from '../../../mol-util/float-packing';
@@ -45,14 +45,11 @@ function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) {
 let SumTexture: Texture
 function getSumTexture(ctx: WebGLContext) {
     if (SumTexture) return SumTexture
-    SumTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'nearest')
+    SumTexture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest')
     SumTexture.define(1, 1)
     return SumTexture
 }
 
-/** name for shared framebuffer used for histogram-pyramid operations */
-const FramebufferName = 'histogram-pyramid-sum'
-
 function setRenderingDefaults(ctx: WebGLContext) {
     const { gl, state } = ctx
     state.disable(gl.CULL_FACE)
@@ -66,12 +63,12 @@ function setRenderingDefaults(ctx: WebGLContext) {
 
 const sumArray = new Uint8Array(4)
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
-    const { gl, framebufferCache } = ctx
+    const { gl, resources } = ctx
 
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture)
     ctx.state.currentRenderItemId = -1
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
     const sumTexture = getSumTexture(ctx)
     sumTexture.attachFramebuffer(framebuffer, 0)
 

+ 6 - 8
src/mol-gl/compute/marching-cubes/active-voxels.ts

@@ -8,7 +8,7 @@ import { createComputeRenderable } from '../../renderable'
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
-import { Texture, createTexture } from '../../../mol-gl/webgl/texture';
+import { Texture } from '../../../mol-gl/webgl/texture';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { Vec3, Vec2 } from '../../../mol-math/linear-algebra';
@@ -17,9 +17,6 @@ import { getTriCount } from './tables';
 import quad_vert from '../../../mol-gl/shader/quad.vert'
 import active_voxels_frag from '../../../mol-gl/shader/marching-cubes/active-voxels.frag'
 
-/** name for shared framebuffer used for gpu marching cubes operations */
-const FramebufferName = 'marching-cubes-active-voxels'
-
 const ActiveVoxelsSchema = {
     ...QuadSchema,
 
@@ -67,13 +64,14 @@ function setRenderingDefaults(ctx: WebGLContext) {
 }
 
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
-    const { gl, framebufferCache } = ctx
-    const { width, height } = volumeData
+    const { gl, resources } = ctx
+    const width = volumeData.getWidth()
+    const height = volumeData.getHeight()
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
     framebuffer.bind()
 
-    const activeVoxelsTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
+    const activeVoxelsTex = resources.texture('image-float32', 'rgba', 'float', 'nearest')
     activeVoxelsTex.define(width, height)
 
     const renderable = getActiveVoxelsRenderable(ctx, volumeData, gridDim, gridTexDim, isoValue, gridScale)

+ 12 - 15
src/mol-gl/compute/marching-cubes/isosurface.ts

@@ -8,7 +8,7 @@ import { createComputeRenderable } from '../../renderable'
 import { WebGLContext } from '../../webgl/context';
 import { createComputeRenderItem } from '../../webgl/render-item';
 import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
-import { Texture, createTexture } from '../../../mol-gl/webgl/texture';
+import { Texture } from '../../../mol-gl/webgl/texture';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { ValueCell } from '../../../mol-util';
 import { Vec3, Vec2, Mat4 } from '../../../mol-math/linear-algebra';
@@ -18,9 +18,6 @@ import { getTriIndices } from './tables';
 import quad_vert from '../../../mol-gl/shader/quad.vert'
 import isosurface_frag from '../../../mol-gl/shader/marching-cubes/isosurface.frag'
 
-/** name for shared framebuffer used for gpu marching cubes operations */
-const FramebufferName = 'marching-cubes-isosurface'
-
 const IsosurfaceSchema = {
     ...QuadSchema,
 
@@ -83,30 +80,30 @@ function setRenderingDefaults(ctx: WebGLContext) {
 }
 
 export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, vertexGroupTexture?: Texture, normalTexture?: Texture) {
-    const { gl, framebufferCache } = ctx
+    const { gl, resources } = ctx
     const { pyramidTex, height, levels, scale, count } = histogramPyramid
 
     // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim)
     // console.log('iso volumeData', volumeData)
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
 
     let needsClear = false
 
     if (!vertexGroupTexture) {
-        vertexGroupTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-        vertexGroupTexture.define(pyramidTex.width, pyramidTex.height)
-    } else if (vertexGroupTexture.width !== pyramidTex.width || vertexGroupTexture.height !== pyramidTex.height) {
-        vertexGroupTexture.define(pyramidTex.width, pyramidTex.height)
+        vertexGroupTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest')
+        vertexGroupTexture.define(pyramidTex.getWidth(), pyramidTex.getHeight())
+    } else if (vertexGroupTexture.getWidth() !== pyramidTex.getWidth() || vertexGroupTexture.getHeight() !== pyramidTex.getHeight()) {
+        vertexGroupTexture.define(pyramidTex.getWidth(), pyramidTex.getHeight())
     } else {
         needsClear = true
     }
 
     if (!normalTexture) {
-        normalTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest')
-        normalTexture.define(pyramidTex.width, pyramidTex.height)
-    } else if (normalTexture.width !== pyramidTex.width || normalTexture.height !== pyramidTex.height) {
-        normalTexture.define(pyramidTex.width, pyramidTex.height)
+        normalTexture = resources.texture('image-float32', 'rgba', 'float', 'nearest')
+        normalTexture.define(pyramidTex.getWidth(), pyramidTex.getHeight())
+    } else if (normalTexture.getWidth() !== pyramidTex.getWidth() || normalTexture.getHeight() !== pyramidTex.getHeight()) {
+        normalTexture.define(pyramidTex.getWidth(), pyramidTex.getHeight())
     } else {
         needsClear = true
     }
@@ -150,7 +147,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     ])
 
     setRenderingDefaults(ctx)
-    gl.viewport(0, 0, pyramidTex.width, pyramidTex.height)
+    gl.viewport(0, 0, pyramidTex.getWidth(), pyramidTex.getHeight())
     if (needsClear) gl.clear(gl.COLOR_BUFFER_BIT)
     renderable.render()
 

+ 6 - 6
src/mol-gl/compute/util.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,7 +13,7 @@ import { Vec2 } from '../../mol-math/linear-algebra';
 import { GLRenderingContext } from '../../mol-gl/webgl/compat';
 
 export const QuadPositions = new Float32Array([
-     1.0,  1.0,  -1.0,  1.0,  -1.0, -1.0, // First triangle
+    1.0,  1.0,  -1.0,  1.0,  -1.0, -1.0, // First triangle
     -1.0, -1.0,   1.0, -1.0,   1.0,  1.0  // Second triangle
 ])
 
@@ -42,11 +42,11 @@ function getArrayForTexture(gl: GLRenderingContext, texture: Texture, size: numb
 }
 
 export function readTexture(ctx: WebGLContext, texture: Texture, width?: number, height?: number) {
-    const { gl, framebufferCache } = ctx
-    width = defaults(width, texture.width)
-    height = defaults(height, texture.height)
+    const { gl, resources } = ctx
+    width = defaults(width, texture.getWidth())
+    height = defaults(height, texture.getHeight())
     const size = width * height * 4
-    const framebuffer = framebufferCache.get('read-texture').value
+    const framebuffer = resources.framebuffer()
     const array = getArrayForTexture(gl, texture, size)
     framebuffer.bind()
     texture.attachFramebuffer(framebuffer, 0)

+ 29 - 43
src/mol-gl/render-object.ts

@@ -1,11 +1,10 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { RenderableState, Renderable } from './renderable'
-import { RenderableValues } from './renderable/schema';
 import { idFactory } from '../mol-util/id-factory';
 import { WebGLContext } from './webgl/context';
 import { DirectVolumeValues, DirectVolumeRenderable } from './renderable/direct-volume';
@@ -20,53 +19,40 @@ const getNextId = idFactory(0, 0x7FFFFFFF)
 
 export const getNextMaterialId = idFactory(0, 0x7FFFFFFF)
 
-export interface BaseRenderObject<T extends RenderableValues> { id: number, type: string, values: T, state: RenderableState, materialId: number }
-export interface MeshRenderObject extends BaseRenderObject<MeshValues> { type: 'mesh' }
-export interface PointsRenderObject extends BaseRenderObject<PointsValues> { type: 'points' }
-export interface SpheresRenderObject extends BaseRenderObject<SpheresValues> { type: 'spheres' }
-export interface TextRenderObject extends BaseRenderObject<TextValues> { type: 'text' }
-export interface LinesRenderObject extends BaseRenderObject<LinesValues> { type: 'lines' }
-export interface DirectVolumeRenderObject extends BaseRenderObject<DirectVolumeValues> { type: 'direct-volume' }
-export interface TextureMeshRenderObject extends BaseRenderObject<TextureMeshValues> { type: 'texture-mesh' }
-
-//
+export interface GraphicsRenderObject<T extends RenderObjectType = RenderObjectType> {
+    readonly id: number,
+    readonly type: T,
+    readonly values: RenderObjectValues<T>,
+    readonly state: RenderableState,
+    readonly materialId: number
+}
 
-export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject | TextureMeshRenderObject
+export type RenderObjectType = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'texture-mesh'
 
-export type RenderObjectKindType = {
-    'mesh': MeshRenderObject
-    'points': PointsRenderObject
-    'spheres': SpheresRenderObject
-    'text': TextRenderObject
-    'lines': LinesRenderObject
-    'direct-volume': DirectVolumeRenderObject
-    'texture-mesh': TextureMeshRenderObject
-}
-export type RenderObjectValuesType = {
-    'mesh': MeshValues
-    'points': PointsValues
-    'spheres': SpheresValues
-    'text': TextValues
-    'lines': LinesValues
-    'direct-volume': DirectVolumeValues
-    'texture-mesh': TextureMeshValues
-}
-export type RenderObjectType = keyof RenderObjectKindType
+export type RenderObjectValues<T extends RenderObjectType> =
+    T extends 'mesh' ? MeshValues :
+        T extends 'points' ? PointsValues :
+            T extends 'spheres' ? SpheresValues :
+                T extends 'text' ? TextValues :
+                    T extends 'lines' ? LinesValues :
+                        T extends 'direct-volume' ? DirectVolumeValues :
+                            T extends 'texture-mesh' ? TextureMeshValues : never
 
 //
 
-export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValuesType[T], state: RenderableState, materialId: number): RenderObjectKindType[T] {
-    return { id: getNextId(), type, values, state, materialId } as RenderObjectKindType[T]
+export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValues<T>, state: RenderableState, materialId: number): GraphicsRenderObject<T> {
+    return { id: getNextId(), type, values, state, materialId } as GraphicsRenderObject<T>
 }
 
-export function createRenderable(ctx: WebGLContext, o: GraphicsRenderObject): Renderable<any> {
+export function createRenderable<T extends RenderObjectType>(ctx: WebGLContext, o: GraphicsRenderObject<T>): Renderable<any> {
     switch (o.type) {
-        case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'points': return PointsRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'text': return TextRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state, o.materialId)
-        case 'texture-mesh': return TextureMeshRenderable(ctx, o.id, o.values, o.state, o.materialId)
+        case 'mesh': return MeshRenderable(ctx, o.id, o.values as MeshValues, o.state, o.materialId)
+        case 'points': return PointsRenderable(ctx, o.id, o.values as PointsValues, o.state, o.materialId)
+        case 'spheres': return SpheresRenderable(ctx, o.id, o.values as SpheresValues, o.state, o.materialId)
+        case 'text': return TextRenderable(ctx, o.id, o.values as TextValues, o.state, o.materialId)
+        case 'lines': return LinesRenderable(ctx, o.id, o.values as LinesValues, o.state, o.materialId)
+        case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values as DirectVolumeValues, o.state, o.materialId)
+        case 'texture-mesh': return TextureMeshRenderable(ctx, o.id, o.values as TextureMeshValues, o.state, o.materialId)
     }
-}
+    throw new Error('unsupported type')
+}

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

@@ -20,7 +20,7 @@ export const MeshSchema = {
     dDoubleSided: DefineSpec('boolean'),
     dFlipSided: DefineSpec('boolean'),
     dIgnoreLight: DefineSpec('boolean'),
-}
+} as const
 export type MeshSchema = typeof MeshSchema
 export type MeshValues = Values<MeshSchema>
 

+ 11 - 11
src/mol-gl/renderable/schema.ts

@@ -62,7 +62,7 @@ export type KindValue = {
     'sphere': Sphere3D
 }
 
-export type Values<S extends RenderableSchema> = { [k in keyof S]: ValueCell<KindValue[S[k]['kind']]> }
+export type Values<S extends RenderableSchema> = { readonly [k in keyof S]: ValueCell<KindValue[S[k]['kind']]> }
 
 export function splitValues(schema: RenderableSchema, values: RenderableValues) {
     const attributeValues: AttributeValues = {}
@@ -128,12 +128,12 @@ export function ValueSpec<K extends ValueKind>(kind: K): ValueSpec<K> {
 //
 
 export type RenderableSchema = {
-    [k: string]: (
+    readonly [k: string]: (
         AttributeSpec<AttributeKind> | UniformSpec<UniformKind> | TextureSpec<TextureKind> |
         ValueSpec<ValueKind> | DefineSpec<DefineKind> | ElementsSpec<ElementsKind>
     )
 }
-export type RenderableValues = { [k: string]: ValueCell<any> }
+export type RenderableValues = { readonly [k: string]: ValueCell<any> }
 
 //
 
@@ -181,14 +181,14 @@ export const GlobalUniformSchema = {
 
     uHighlightColor: UniformSpec('v3'),
     uSelectColor: UniformSpec('v3'),
-}
+} as const
 export type GlobalUniformSchema = typeof GlobalUniformSchema
 export type GlobalUniformValues = Values<GlobalUniformSchema> // { [k in keyof GlobalUniformSchema]: ValueCell<any> }
 
 export const InternalSchema = {
     uObjectId: UniformSpec('i'),
     uPickable: UniformSpec('i', true),
-}
+} as const
 export type InternalSchema = typeof InternalSchema
 export type InternalValues = { [k in keyof InternalSchema]: ValueCell<any> }
 
@@ -198,7 +198,7 @@ export const ColorSchema = {
     uColorTexDim: UniformSpec('v2'),
     tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
     dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']),
-}
+} as const
 export type ColorSchema = typeof ColorSchema
 export type ColorValues = Values<ColorSchema>
 
@@ -209,14 +209,14 @@ export const SizeSchema = {
     tSize: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dSizeType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']),
     uSizeFactor: UniformSpec('f'),
-}
+} as const
 export type SizeSchema = typeof SizeSchema
 export type SizeValues = Values<SizeSchema>
 
 export const MarkerSchema = {
     uMarkerTexDim: UniformSpec('v2'),
     tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
-}
+} as const
 export type MarkerSchema = typeof MarkerSchema
 export type MarkerValues = Values<MarkerSchema>
 
@@ -224,7 +224,7 @@ export const OverpaintSchema = {
     uOverpaintTexDim: UniformSpec('v2'),
     tOverpaint: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
     dOverpaint: DefineSpec('boolean'),
-}
+} as const
 export type OverpaintSchema = typeof OverpaintSchema
 export type OverpaintValues = Values<OverpaintSchema>
 
@@ -235,7 +235,7 @@ export const TransparencySchema = {
     dTransparency: DefineSpec('boolean'),
     // dTransparencyType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), // TODO
     dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
-}
+} as const
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>
 
@@ -277,6 +277,6 @@ export const BaseSchema = {
     boundingSphere: ValueSpec('sphere'),
     /** bounding sphere NOT taking aTransform into account */
     invariantBoundingSphere: ValueSpec('sphere'),
-}
+} as const
 export type BaseSchema = typeof BaseSchema
 export type BaseValues = Values<BaseSchema>

+ 12 - 10
src/mol-gl/renderer.ts

@@ -22,11 +22,12 @@ export interface RendererStats {
     programCount: number
     shaderCount: number
 
-    bufferCount: number
+    attributeCount: number
+    elementsCount: number
     framebufferCount: number
     renderbufferCount: number
     textureCount: number
-    vaoCount: number
+    vertexArrayCount: number
 
     drawCount: number
     instanceCount: number
@@ -320,14 +321,15 @@ namespace Renderer {
             },
             get stats(): RendererStats {
                 return {
-                    programCount: ctx.programCache.count,
-                    shaderCount: ctx.shaderCache.count,
-
-                    bufferCount: stats.bufferCount,
-                    framebufferCount: stats.framebufferCount,
-                    renderbufferCount: stats.renderbufferCount,
-                    textureCount: stats.textureCount,
-                    vaoCount: stats.vaoCount,
+                    programCount: ctx.stats.resourceCounts.program,
+                    shaderCount: ctx.stats.resourceCounts.shader,
+
+                    attributeCount: ctx.stats.resourceCounts.attribute,
+                    elementsCount: ctx.stats.resourceCounts.elements,
+                    framebufferCount: ctx.stats.resourceCounts.framebuffer,
+                    renderbufferCount: ctx.stats.resourceCounts.renderbuffer,
+                    textureCount: ctx.stats.resourceCounts.texture,
+                    vertexArrayCount: ctx.stats.resourceCounts.vertexArray,
 
                     drawCount: stats.drawCount,
                     instanceCount: stats.instanceCount,

+ 6 - 2
src/mol-gl/shader-code.ts

@@ -201,13 +201,17 @@ const glsl300FragPrefixCommon = `
 #define gl_FragColor out_FragData0
 #define gl_FragDepthEXT gl_FragDepth
 
-#define enabledStandardDerivatives
-#define enabledFragDepth
 #define requiredDrawBuffers
 `
 
 function getGlsl300FragPrefix(gl: WebGL2RenderingContext, extensions: WebGLExtensions, shaderExtensions: ShaderExtensions) {
     const prefix = [ '#version 300 es' ]
+    if (shaderExtensions.standardDerivatives) {
+        prefix.push('#define enabledStandardDerivatives')
+    }
+    if (shaderExtensions.fragDepth) {
+        prefix.push('#define enabledFragDepth')
+    }
     if (extensions.drawBuffers) {
         const maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS) as number
         for (let i = 0, il = maxDrawBuffers; i < il; ++i) {

+ 1 - 1
src/mol-gl/shader/chunks/common.glsl.ts

@@ -11,7 +11,7 @@ float intMod(const in float a, const in float b) { return a - b * float(int(a) /
 float pow2(const in float x) { return x*x; }
 
 const float maxFloat = 10000.0; // NOTE constant also set in TypeScript
-const float floatLogFactor = log(maxFloat + 1.0);
+const float floatLogFactor = 9.210440366976517; // log(maxFloat + 1.0);
 float encodeFloatLog(const in float value) { return log(value + 1.0) / floatLogFactor; }
 float decodeFloatLog(const in float value) { return exp(value * floatLogFactor) - 1.0; }
 

+ 34 - 32
src/mol-gl/webgl/buffer.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,6 +10,7 @@ import { RenderableSchema } from '../renderable/schema';
 import { idFactory } from '../../mol-util/id-factory';
 import { ValueOf } from '../../mol-util/type-helpers';
 import { GLRenderingContext } from './compat';
+import { WebGLExtensions } from './extensions';
 
 const getNextBufferId = idFactory()
 
@@ -29,8 +30,7 @@ export type DataTypeArrayType = {
 export type ArrayType = ValueOf<DataTypeArrayType>
 export type ArrayKind = keyof DataTypeArrayType
 
-export function getUsageHint(ctx: WebGLContext, usageHint: UsageHint) {
-    const { gl } = ctx
+export function getUsageHint(gl: GLRenderingContext, usageHint: UsageHint) {
     switch (usageHint) {
         case 'static': return gl.STATIC_DRAW
         case 'dynamic': return gl.DYNAMIC_DRAW
@@ -38,8 +38,7 @@ export function getUsageHint(ctx: WebGLContext, usageHint: UsageHint) {
     }
 }
 
-export function getDataType(ctx: WebGLContext, dataType: DataType) {
-    const { gl } = ctx
+export function getDataType(gl: GLRenderingContext, dataType: DataType) {
     switch (dataType) {
         case 'uint8': return gl.UNSIGNED_BYTE
         case 'int8': return gl.BYTE
@@ -51,8 +50,7 @@ export function getDataType(ctx: WebGLContext, dataType: DataType) {
     }
 }
 
-function dataTypeFromArray(ctx: WebGLContext, array: ArrayType) {
-    const { gl } = ctx
+function dataTypeFromArray(gl: GLRenderingContext, array: ArrayType) {
     if (array instanceof Uint8Array) {
         return gl.UNSIGNED_BYTE
     } else if (array instanceof Int8Array) {
@@ -72,8 +70,7 @@ function dataTypeFromArray(ctx: WebGLContext, array: ArrayType) {
     }
 }
 
-export function getBufferType(ctx: WebGLContext, bufferType: BufferType) {
-    const { gl } = ctx
+export function getBufferType(gl: GLRenderingContext, bufferType: BufferType) {
     switch (bufferType) {
         case 'attribute': return gl.ARRAY_BUFFER
         case 'elements': return gl.ELEMENT_ARRAY_BUFFER
@@ -84,7 +81,6 @@ export function getBufferType(ctx: WebGLContext, bufferType: BufferType) {
 export interface Buffer {
     readonly id: number
 
-    readonly _buffer: WebGLBuffer
     readonly _usageHint: number
     readonly _bufferType: number
     readonly _dataType: number
@@ -92,21 +88,28 @@ export interface Buffer {
 
     readonly length: number
 
+    getBuffer: () => WebGLBuffer
     updateData: (array: ArrayType) => void
     updateSubData: (array: ArrayType, offset: number, count: number) => void
+
+    reset: () => void
     destroy: () => void
 }
 
-export function createBuffer(ctx: WebGLContext, array: ArrayType, usageHint: UsageHint, bufferType: BufferType): Buffer {
-    const { gl, stats } = ctx
-    const _buffer = gl.createBuffer()
-    if (_buffer === null) {
+function getBuffer(gl: GLRenderingContext) {
+    const buffer = gl.createBuffer()
+    if (buffer === null) {
         throw new Error('Could not create WebGL buffer')
     }
+    return buffer
+}
 
-    const _usageHint = getUsageHint(ctx, usageHint)
-    const _bufferType = getBufferType(ctx, bufferType)
-    const _dataType = dataTypeFromArray(ctx, array)
+function createBuffer(gl: GLRenderingContext, array: ArrayType, usageHint: UsageHint, bufferType: BufferType): Buffer {
+    let _buffer = getBuffer(gl)
+
+    const _usageHint = getUsageHint(gl, usageHint)
+    const _bufferType = getBufferType(gl, bufferType)
+    const _dataType = dataTypeFromArray(gl, array)
     const _bpe = array.BYTES_PER_ELEMENT
     const _length = array.length
 
@@ -117,18 +120,17 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, usageHint: Usa
     updateData(array)
 
     let destroyed = false
-    stats.bufferCount += 1
 
     return {
         id: getNextBufferId(),
 
-        _buffer,
         _usageHint,
         _bufferType,
         _dataType,
         _bpe,
 
         length: _length,
+        getBuffer: () => _buffer,
 
         updateData,
         updateSubData: (array: ArrayType, offset: number, count: number) => {
@@ -136,11 +138,14 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, usageHint: Usa
             gl.bufferSubData(_bufferType, offset * _bpe, array.subarray(offset, offset + count))
         },
 
+        reset: () => {
+            _buffer = getBuffer(gl)
+            updateData(array)
+        },
         destroy: () => {
             if (destroyed) return
             gl.deleteBuffer(_buffer)
             destroyed = true
-            stats.bufferCount -= 1
         }
     }
 }
@@ -183,17 +188,16 @@ export interface AttributeBuffer extends Buffer {
     bind: (location: number) => void
 }
 
-export function createAttributeBuffer<T extends ArrayType, S extends AttributeItemSize>(ctx: WebGLContext, array: T, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer {
-    const { gl } = ctx
-    const { instancedArrays } = ctx.extensions
+export function createAttributeBuffer<T extends ArrayType, S extends AttributeItemSize>(gl: GLRenderingContext, extensions: WebGLExtensions, array: T, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer {
+    const { instancedArrays } = extensions
 
-    const buffer = createBuffer(ctx, array, usageHint, 'attribute')
-    const { _buffer, _bufferType, _dataType, _bpe } = buffer
+    const buffer = createBuffer(gl, array, usageHint, 'attribute')
+    const { _bufferType, _dataType, _bpe } = buffer
 
     return {
         ...buffer,
         bind: (location: number) => {
-            gl.bindBuffer(_bufferType, _buffer)
+            gl.bindBuffer(_bufferType, buffer.getBuffer())
             if (itemSize === 16) {
                 for (let i = 0; i < 4; ++i) {
                     gl.enableVertexAttribArray(location + i)
@@ -214,7 +218,7 @@ export function createAttributeBuffers(ctx: WebGLContext, schema: RenderableSche
     Object.keys(schema).forEach(k => {
         const spec = schema[k]
         if (spec.type === 'attribute') {
-            buffers[buffers.length] = [k, createAttributeBuffer(ctx, values[k].ref.value, spec.itemSize, spec.divisor)]
+            buffers[buffers.length] = [k, ctx.resources.attribute(values[k].ref.value, spec.itemSize, spec.divisor)]
         }
     })
     return buffers
@@ -229,15 +233,13 @@ export interface ElementsBuffer extends Buffer {
     bind: () => void
 }
 
-export function createElementsBuffer(ctx: WebGLContext, array: ElementsType, usageHint: UsageHint = 'static'): ElementsBuffer {
-    const { gl } = ctx
-    const buffer = createBuffer(ctx, array, usageHint, 'elements')
-    const { _buffer } = buffer
+export function createElementsBuffer(gl: GLRenderingContext, array: ElementsType, usageHint: UsageHint = 'static'): ElementsBuffer {
+    const buffer = createBuffer(gl, array, usageHint, 'elements')
 
     return {
         ...buffer,
         bind: () => {
-            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, _buffer);
+            gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer.getBuffer());
         }
     }
 }

+ 72 - 41
src/mol-gl/webgl/context.ts

@@ -1,23 +1,25 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createProgramCache, ProgramCache } from './program'
-import { createShaderCache, ShaderCache } from './shader'
 import { GLRenderingContext, isWebGL2 } from './compat';
-import { createFramebufferCache, FramebufferCache, checkFramebufferStatus } from './framebuffer';
+import { checkFramebufferStatus } from './framebuffer';
 import { Scheduler } from '../../mol-task';
 import { isDebugMode } from '../../mol-util/debug';
 import { createExtensions, WebGLExtensions } from './extensions';
 import { WebGLState, createState } from './state';
 import { PixelData } from '../../mol-util/image';
+import { WebGLResources, createResources } from './resources';
+import { RenderTarget, createRenderTarget } from './render-target';
+import { BehaviorSubject } from 'rxjs';
+import { now } from '../../mol-util/now';
 
 export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
     function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
         try {
-           return canvas.getContext(contextId, contextAttributes) as GLRenderingContext | null
+            return canvas.getContext(contextId, contextAttributes) as GLRenderingContext | null
         } catch (e) {
             return null
         }
@@ -44,7 +46,9 @@ function getErrorDescription(gl: GLRenderingContext, error: number) {
 
 export function checkError(gl: GLRenderingContext) {
     const error = gl.getError()
-    if (error) throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`)
+    if (error !== gl.NO_ERROR) {
+        throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`)
+    }
 }
 
 function unbindResources (gl: GLRenderingContext) {
@@ -123,7 +127,7 @@ function waitForGpuCommandsCompleteSync(gl: GLRenderingContext): void {
     gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel)
 }
 
-function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) {
+export function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) {
     if (isDebugMode) checkFramebufferStatus(gl)
     if (buffer instanceof Uint8Array) {
         gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer)
@@ -147,25 +151,18 @@ function getDrawingBufferPixelData(gl: GLRenderingContext) {
 
 //
 
-export type WebGLStats = {
-    bufferCount: number
-    framebufferCount: number
-    renderbufferCount: number
-    textureCount: number
-    vaoCount: number
-
-    drawCount: number
-    instanceCount: number
-    instancedDrawCount: number
-}
-
-function createStats(): WebGLStats {
+function createStats() {
     return {
-        bufferCount: 0,
-        framebufferCount: 0,
-        renderbufferCount: 0,
-        textureCount: 0,
-        vaoCount: 0,
+        resourceCounts: {
+            attribute: 0,
+            elements: 0,
+            framebuffer: 0,
+            program: 0,
+            renderbuffer: 0,
+            shader: 0,
+            texture: 0,
+            vertexArray: 0,
+        },
 
         drawCount: 0,
         instanceCount: 0,
@@ -173,6 +170,8 @@ function createStats(): WebGLStats {
     }
 }
 
+export type WebGLStats = ReturnType<typeof createStats>
+
 //
 
 /** A WebGL context object, including the rendering context, resource caches and counts */
@@ -184,15 +183,18 @@ export interface WebGLContext {
     readonly extensions: WebGLExtensions
     readonly state: WebGLState
     readonly stats: WebGLStats
-
-    readonly shaderCache: ShaderCache
-    readonly programCache: ProgramCache
-    readonly framebufferCache: FramebufferCache
+    readonly resources: WebGLResources
 
     readonly maxTextureSize: number
     readonly maxRenderbufferSize: number
     readonly maxDrawBuffers: number
 
+    readonly isContextLost: boolean
+    readonly contextRestored: BehaviorSubject<now.Timestamp>
+    setContextLost: () => void
+    handleContextRestored: () => void
+
+    createRenderTarget: (width: number, height: number) => RenderTarget
     unbindFramebuffer: () => void
     readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void
     readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
@@ -206,10 +208,7 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
     const extensions = createExtensions(gl)
     const state = createState(gl)
     const stats = createStats()
-
-    const shaderCache: ShaderCache = createShaderCache(gl)
-    const programCache: ProgramCache = createProgramCache(gl, state, extensions, shaderCache)
-    const framebufferCache: FramebufferCache = createFramebufferCache(gl, stats)
+    const resources = createResources(gl, state, stats, extensions)
 
     const parameters = {
         maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
@@ -222,6 +221,9 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8')
     }
 
+    let isContextLost = false
+    let contextRestored = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
+
     let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void>
     if (isWebGL2(gl)) {
         const pbo = gl.createBuffer()
@@ -259,6 +261,8 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         }
     }
 
+    const renderTargets = new Set<RenderTarget>()
+
     return {
         gl,
         isWebGL2: isWebGL2(gl),
@@ -270,15 +274,45 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         extensions,
         state,
         stats,
-
-        shaderCache,
-        programCache,
-        framebufferCache,
+        resources,
 
         get maxTextureSize () { return parameters.maxTextureSize },
         get maxRenderbufferSize () { return parameters.maxRenderbufferSize },
         get maxDrawBuffers () { return parameters.maxDrawBuffers },
 
+        get isContextLost () {
+            return isContextLost || gl.isContextLost()
+        },
+        contextRestored,
+        setContextLost: () => {
+            isContextLost = true
+        },
+        handleContextRestored: () => {
+            Object.assign(extensions, createExtensions(gl))
+
+            state.reset()
+            state.currentMaterialId = -1
+            state.currentProgramId = -1
+            state.currentRenderItemId = -1
+
+            resources.reset()
+            renderTargets.forEach(rt => rt.reset())
+
+            isContextLost = false
+            contextRestored.next(now())
+        },
+
+        createRenderTarget: (width: number, height: number) => {
+            const renderTarget = createRenderTarget(gl, resources, width, height)
+            renderTargets.add(renderTarget)
+            return {
+                ...renderTarget,
+                destroy: () => {
+                    renderTarget.destroy()
+                    renderTargets.delete(renderTarget)
+                }
+            }
+        },
         unbindFramebuffer: () => unbindFramebuffer(gl),
         readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => {
             readPixels(gl, x, y, width, height, buffer)
@@ -289,11 +323,8 @@ export function createContext(gl: GLRenderingContext): WebGLContext {
         getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
 
         destroy: () => {
+            resources.destroy()
             unbindResources(gl)
-            programCache.dispose()
-            shaderCache.dispose()
-            framebufferCache.dispose()
-            // TODO destroy buffers and textures
         }
     }
 }

+ 14 - 19
src/mol-gl/webgl/framebuffer.ts

@@ -1,12 +1,10 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLStats } from './context'
 import { idFactory } from '../../mol-util/id-factory';
-import { ReferenceCache, createReferenceCache } from '../../mol-util/reference-cache';
 import { GLRenderingContext, isWebGL2 } from './compat';
 
 const getNextFramebufferId = idFactory()
@@ -40,37 +38,34 @@ export interface Framebuffer {
     readonly id: number
 
     bind: () => void
+    reset: () => void
     destroy: () => void
 }
 
-export function createFramebuffer (gl: GLRenderingContext, stats: WebGLStats): Framebuffer {
-    const _framebuffer = gl.createFramebuffer()
-    if (_framebuffer === null) {
+function getFramebuffer(gl: GLRenderingContext) {
+    const framebuffer = gl.createFramebuffer()
+    if (framebuffer === null) {
         throw new Error('Could not create WebGL framebuffer')
     }
+    return framebuffer
+}
+
+export function createFramebuffer (gl: GLRenderingContext): Framebuffer {
+    let _framebuffer = getFramebuffer(gl)
 
     let destroyed = false
-    stats.framebufferCount += 1
 
     return {
         id: getNextFramebufferId(),
-
         bind: () => gl.bindFramebuffer(gl.FRAMEBUFFER, _framebuffer),
+
+        reset: () => {
+            _framebuffer = getFramebuffer(gl)
+        },
         destroy: () => {
             if (destroyed) return
             gl.deleteFramebuffer(_framebuffer)
             destroyed = true
-            stats.framebufferCount -= 1
         }
     }
-}
-
-export type FramebufferCache = ReferenceCache<Framebuffer, string>
-
-export function createFramebufferCache(gl: GLRenderingContext, stats: WebGLStats): FramebufferCache {
-    return createReferenceCache(
-        (name: string) => name,
-        () => createFramebuffer(gl, stats),
-        (framebuffer: Framebuffer) => { framebuffer.destroy() }
-    )
 }

+ 47 - 49
src/mol-gl/webgl/program.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,16 +7,14 @@
 import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code'
 import { WebGLState } from './state';
 import { WebGLExtensions } from './extensions';
-import { getUniformSetters, UniformsList, getUniformType } from './uniform';
+import { getUniformSetters, UniformsList, getUniformType, UniformSetters } from './uniform';
 import { AttributeBuffers, getAttribType } from './buffer';
 import { TextureId, Textures } from './texture';
-import { createReferenceCache, ReferenceCache } from '../../mol-util/reference-cache';
 import { idFactory } from '../../mol-util/id-factory';
 import { RenderableSchema } from '../renderable/schema';
-import { hashFnv32a, hashString } from '../../mol-data/util';
 import { isDebugMode } from '../../mol-util/debug';
 import { GLRenderingContext } from './compat';
-import { ShaderCache } from './shader';
+import { ShaderType, Shader } from './shader';
 
 const getNextProgramId = idFactory()
 
@@ -28,6 +26,7 @@ export interface Program {
     bindAttributes: (attribueBuffers: AttributeBuffers) => void
     bindTextures: (textures: Textures) => void
 
+    reset: () => void
     destroy: () => void
 }
 
@@ -113,43 +112,60 @@ function checkActiveUniforms(gl: GLRenderingContext, program: WebGLProgram, sche
     }
 }
 
+function checkProgram(gl: GLRenderingContext, program: WebGLProgram) {
+    // no-op in FF on Mac, see https://bugzilla.mozilla.org/show_bug.cgi?id=1284425
+    // gl.validateProgram(program)
+    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+        throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`);
+    }
+}
+
 export interface ProgramProps {
     defineValues: DefineValues,
     shaderCode: ShaderCode,
     schema: RenderableSchema
 }
 
-export function createProgram(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache, props: ProgramProps): Program {
-    const { defineValues, shaderCode: _shaderCode, schema } = props
-
+function getProgram(gl: GLRenderingContext) {
     const program = gl.createProgram()
     if (program === null) {
         throw new Error('Could not create WebGL program')
     }
+    return program
+}
+
+type ShaderGetter = (type: ShaderType, source: string) => Shader
+
+export function createProgram(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, getShader: ShaderGetter, props: ProgramProps): Program {
+    const { defineValues, shaderCode: _shaderCode, schema } = props
+
+    let program = getProgram(gl)
     const programId = getNextProgramId()
 
     const shaderCode = addShaderDefines(gl, extensions, defineValues, _shaderCode)
-    const vertShaderRef = shaderCache.get({ type: 'vert', source: shaderCode.vert })
-    const fragShaderRef = shaderCache.get({ type: 'frag', source: shaderCode.frag })
-
-    vertShaderRef.value.attach(program)
-    fragShaderRef.value.attach(program)
-    gl.linkProgram(program)
-    if (isDebugMode) {
-        // no-op in FF on Mac, see https://bugzilla.mozilla.org/show_bug.cgi?id=1284425
-        // gl.validateProgram(program)
-        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
-            throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`);
+    const vertShader = getShader('vert', shaderCode.vert)
+    const fragShader = getShader('frag', shaderCode.frag)
+
+    let locations: Locations // = getLocations(gl, program, schema)
+    let uniformSetters: UniformSetters // = getUniformSetters(schema)
+
+    function init() {
+        vertShader.attach(program)
+        fragShader.attach(program)
+        gl.linkProgram(program)
+        if (isDebugMode) {
+            checkProgram(gl, program)
         }
-    }
 
-    const locations = getLocations(gl, program, schema)
-    const uniformSetters = getUniformSetters(schema)
+        locations = getLocations(gl, program, schema)
+        uniformSetters = getUniformSetters(schema)
 
-    if (isDebugMode) {
-        checkActiveAttributes(gl, program, schema)
-        checkActiveUniforms(gl, program, schema)
+        if (isDebugMode) {
+            checkActiveAttributes(gl, program, schema)
+            checkActiveUniforms(gl, program, schema)
+        }
     }
+    init()
 
     let destroyed = false
 
@@ -190,34 +206,16 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi
             }
         },
 
+        reset: () => {
+            program = getProgram(gl)
+            init()
+        },
         destroy: () => {
             if (destroyed) return
-            vertShaderRef.free()
-            fragShaderRef.free()
+            vertShader.destroy()
+            fragShader.destroy()
             gl.deleteProgram(program)
             destroyed = true
         }
     }
-}
-
-export type ProgramCache = ReferenceCache<Program, ProgramProps>
-
-function defineValueHash(v: boolean | number | string): number {
-    return typeof v === 'boolean' ? (v ? 1 : 0) :
-        typeof v === 'number' ? v : hashString(v)
-}
-
-export function createProgramCache(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache): ProgramCache {
-    return createReferenceCache(
-        (props: ProgramProps) => {
-            const array = [ props.shaderCode.id ]
-            Object.keys(props.defineValues).forEach(k => {
-                const v = props.defineValues[k].ref.value
-                array.push(hashString(k), defineValueHash(v))
-            })
-            return hashFnv32a(array).toString()
-        },
-        (props: ProgramProps) => createProgram(gl, state, extensions, shaderCache, props),
-        (program: Program) => { program.destroy() }
-    )
 }

+ 25 - 42
src/mol-gl/webgl/render-item.ts

@@ -1,22 +1,21 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, AttributeKind } from './buffer';
+import { createAttributeBuffers, ElementsBuffer, AttributeKind } from './buffer';
 import { createTextures, Texture } from './texture';
 import { WebGLContext, checkError } from './context';
 import { ShaderCode } from '../shader-code';
 import { Program } from './program';
 import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema';
 import { idFactory } from '../../mol-util/id-factory';
-import { deleteVertexArray, createVertexArray } from './vertex-array';
 import { ValueCell } from '../../mol-util';
-import { ReferenceItem } from '../../mol-util/reference-cache';
 import { TextureImage, TextureVolume } from '../../mol-gl/renderable/util';
 import { checkFramebufferStatus } from './framebuffer';
 import { isDebugMode } from '../../mol-util/debug';
+import { VertexArray } from './vertex-array';
 
 const getNextRenderItemId = idFactory()
 
@@ -65,8 +64,8 @@ type RenderVariantDefines = typeof GraphicsRenderVariantDefines | typeof Compute
 
 //
 
-type ProgramVariants = { [k: string]: ReferenceItem<Program> }
-type VertexArrayVariants = { [k: string]: WebGLVertexArrayObjectOES | null }
+type ProgramVariants = { [k: string]: Program }
+type VertexArrayVariants = { [k: string]: VertexArray | null }
 
 interface ValueChanges {
     attributes: boolean
@@ -108,7 +107,7 @@ export function createComputeRenderItem(ctx: WebGLContext, drawMode: DrawMode, s
  */
 export function createRenderItem<T extends RenderVariantDefines, S extends keyof T & string>(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number, renderVariantDefines: T): RenderItem<S> {
     const id = getNextRenderItemId()
-    const { stats, state, programCache } = ctx
+    const { stats, state, resources } = ctx
     const { instancedArrays, vertexArrayObject } = ctx.extensions
 
     const { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues } = splitValues(schema, values)
@@ -124,11 +123,7 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
     const programs: ProgramVariants = {}
     Object.keys(renderVariantDefines).forEach(k => {
         const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k]
-        programs[k] = programCache.get({
-            defineValues: { ...defineValues, ...variantDefineValues },
-            shaderCode,
-            schema
-        })
+        programs[k] = resources.program({ ...defineValues, ...variantDefineValues }, shaderCode, schema)
     })
 
     const textures = createTextures(ctx, schema, textureValues)
@@ -137,12 +132,12 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
     let elementsBuffer: ElementsBuffer | undefined
     const elements = values.elements
     if (elements && elements.ref.value) {
-        elementsBuffer = createElementsBuffer(ctx, elements.ref.value)
+        elementsBuffer = resources.elements(elements.ref.value)
     }
 
     const vertexArrays: VertexArrayVariants = {}
     Object.keys(renderVariantDefines).forEach(k => {
-        vertexArrays[k] = createVertexArray(ctx, programs[k].value, attributeBuffers, elementsBuffer)
+        vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null
     })
 
     let drawCount = values.drawCount.ref.value
@@ -160,11 +155,11 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
     return {
         id,
         materialId,
-        getProgram: (variant: S) => programs[variant].value,
+        getProgram: (variant: S) => programs[variant],
 
         render: (variant: S) => {
-            if (drawCount === 0 || instanceCount === 0) return
-            const program = programs[variant].value
+            if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return
+            const program = programs[variant]
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
                 program.setUniforms(uniformValueEntries)
                 program.bindTextures(textures)
@@ -181,8 +176,8 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
                 }
                 program.setUniforms(uniformValueEntries)
                 program.bindTextures(textures)
-                if (vertexArrayObject && vertexArray) {
-                    vertexArrayObject.bindVertexArray(vertexArray)
+                if (vertexArray) {
+                    vertexArray.bind()
                     // need to bind elements buffer explicitly since it is not always recorded in the VAO
                     if (elementsBuffer) elementsBuffer.bind()
                 } else {
@@ -226,12 +221,8 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
                 // console.log('some defines changed, need to rebuild programs')
                 Object.keys(renderVariantDefines).forEach(k => {
                     const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k]
-                    programs[k].free()
-                    programs[k] = programCache.get({
-                        defineValues: { ...defineValues, ...variantDefineValues },
-                        shaderCode,
-                        schema
-                    })
+                    programs[k].destroy()
+                    programs[k] = resources.program({ ...defineValues, ...variantDefineValues }, shaderCode, schema)
                 })
             }
 
@@ -261,7 +252,7 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
                         // console.log('attribute array to small, need to create new attribute', k, value.ref.id, value.ref.version)
                         buffer.destroy()
                         const { itemSize, divisor } = schema[k] as AttributeSpec<AttributeKind>
-                        attributeBuffers[i][1] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor)
+                        attributeBuffers[i][1] = resources.attribute(value.ref.value, itemSize, divisor)
                         valueChanges.attributes = true
                     }
                     versions[k] = value.ref.version
@@ -275,7 +266,7 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
                 } else {
                     // console.log('elements array to small, need to create new elements', values.elements.ref.id, values.elements.ref.version)
                     elementsBuffer.destroy()
-                    elementsBuffer = createElementsBuffer(ctx, values.elements.ref.value)
+                    elementsBuffer = resources.elements(values.elements.ref.value)
                     valueChanges.elements = true
                 }
                 versions.elements = values.elements.ref.version
@@ -283,19 +274,10 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
 
             if (valueChanges.attributes || valueChanges.defines || valueChanges.elements) {
                 // console.log('program/defines or buffers changed, update vaos')
-                const { vertexArrayObject } = ctx.extensions
-                if (vertexArrayObject) {
-                    Object.keys(renderVariantDefines).forEach(k => {
-                        vertexArrayObject.bindVertexArray(vertexArrays[k])
-                        if (elementsBuffer && (valueChanges.defines || valueChanges.elements)) {
-                            elementsBuffer.bind()
-                        }
-                        if (valueChanges.attributes || valueChanges.defines) {
-                            programs[k].value.bindAttributes(attributeBuffers)
-                        }
-                        vertexArrayObject.bindVertexArray(null)
-                    })
-                }
+                Object.keys(renderVariantDefines).forEach(k => {
+                    const vertexArray = vertexArrays[k]
+                    if (vertexArray) vertexArray.update()
+                })
             }
 
             for (let i = 0, il = textures.length; i < il; ++i) {
@@ -319,8 +301,9 @@ export function createRenderItem<T extends RenderVariantDefines, S extends keyof
         destroy: () => {
             if (!destroyed) {
                 Object.keys(renderVariantDefines).forEach(k => {
-                    programs[k].free()
-                    deleteVertexArray(ctx, vertexArrays[k])
+                    programs[k].destroy()
+                    const vertexArray = vertexArrays[k]
+                    if (vertexArray) vertexArray.destroy()
                 })
                 textures.forEach(([k, texture]) => {
                     // lifetime of textures with kind 'texture' is defined externally

+ 26 - 22
src/mol-gl/webgl/render-target.ts

@@ -1,63 +1,64 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from './context'
+import { readPixels } from './context'
 import { idFactory } from '../../mol-util/id-factory';
-import { createTexture, Texture } from './texture';
-import { createFramebuffer, Framebuffer } from './framebuffer';
-import { createRenderbuffer } from './renderbuffer';
+import { Texture } from './texture';
+import { Framebuffer } from './framebuffer';
 import { TextureImage } from '../renderable/util';
 import { Mutable } from '../../mol-util/type-helpers';
 import { PixelData } from '../../mol-util/image';
+import { WebGLResources } from './resources';
+import { GLRenderingContext } from './compat';
 
 const getNextRenderTargetId = idFactory()
 
 export interface RenderTarget {
     readonly id: number
-    readonly width: number
-    readonly height: number
     readonly image: TextureImage<any>
     readonly texture: Texture
     readonly framebuffer: Framebuffer
 
+    getWidth: () => number
+    getHeight: () => number
     /** binds framebuffer and sets viewport to rendertarget's width and height */
     bind: () => void
     setSize: (width: number, height: number) => void
     readBuffer: (x: number, y: number, width: number, height: number, dst: Uint8Array) => void
     getBuffer: () => Uint8Array
     getPixelData: () => PixelData
+    reset: () => void
     destroy: () => void
 }
 
-export function createRenderTarget (ctx: WebGLContext, _width: number, _height: number): RenderTarget {
-    const { gl, stats } = ctx
-
+export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResources, _width: number, _height: number): RenderTarget {
     const image: Mutable<TextureImage<Uint8Array>> = {
         array: new Uint8Array(_width * _height * 4),
         width: _width,
         height: _height
     }
 
-    const targetTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'linear')
-    targetTexture.load(image)
-
-    const framebuffer = createFramebuffer(gl, stats)
-
-    // attach the texture as the first color attachment
-    targetTexture.attachFramebuffer(framebuffer, 'color0')
-
+    const framebuffer = resources.framebuffer()
+    const targetTexture = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear')
     // make a depth renderbuffer of the same size as the targetTexture
-    const depthRenderbuffer = createRenderbuffer(ctx, 'depth16', 'depth', _width, _height)
+    const depthRenderbuffer = resources.renderbuffer('depth16', 'depth', _width, _height)
+
+    function init() {
+        targetTexture.load(image)
+        targetTexture.attachFramebuffer(framebuffer, 'color0')
+        depthRenderbuffer.attachFramebuffer(framebuffer)
+    }
+    init()
 
     let destroyed = false
 
     function readBuffer(x: number, y: number, width: number, height: number, dst: Uint8Array) {
         framebuffer.bind()
         gl.viewport(0, 0, _width, _height)
-        ctx.readPixels(x, y, width, height, dst)
+        readPixels(gl, x, y, width, height, dst)
     }
 
     function getBuffer() {
@@ -67,12 +68,12 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height:
 
     return {
         id: getNextRenderTargetId(),
-        get width () { return _width },
-        get height () { return _height },
         image,
         texture: targetTexture,
         framebuffer,
 
+        getWidth: () => _width,
+        getHeight: () => _height,
         bind: () => {
             framebuffer.bind()
             gl.viewport(0, 0, _width, _height)
@@ -89,6 +90,9 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height:
         readBuffer,
         getBuffer,
         getPixelData: () => PixelData.flipY(PixelData.create(getBuffer(), _width, _height)),
+        reset: () => {
+            init()
+        },
         destroy: () => {
             if (destroyed) return
             targetTexture.destroy()

+ 36 - 21
src/mol-gl/webgl/renderbuffer.ts

@@ -1,19 +1,20 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from './context'
 import { idFactory } from '../../mol-util/id-factory';
+import { GLRenderingContext } from './compat';
+import { Framebuffer, checkFramebufferStatus } from './framebuffer';
+import { isDebugMode } from '../../mol-util/debug';
 
 const getNextRenderbufferId = idFactory()
 
 export type RenderbufferFormat = 'depth16' | 'stencil8' | 'rgba4' | 'depth-stencil'
 export type RenderbufferAttachment = 'depth' | 'stencil' | 'depth-stencil' | 'color0'
 
-export function getFormat(ctx: WebGLContext, format: RenderbufferFormat) {
-    const { gl } = ctx
+export function getFormat(gl: GLRenderingContext, format: RenderbufferFormat) {
     switch (format) {
         case 'depth16': return gl.DEPTH_COMPONENT16
         case 'stencil8': return gl.STENCIL_INDEX8
@@ -22,8 +23,7 @@ export function getFormat(ctx: WebGLContext, format: RenderbufferFormat) {
     }
 }
 
-export function getAttachment(ctx: WebGLContext, attachment: RenderbufferAttachment) {
-    const { gl } = ctx
+export function getAttachment(gl: GLRenderingContext, attachment: RenderbufferAttachment) {
     switch (attachment) {
         case 'depth': return gl.DEPTH_ATTACHMENT
         case 'stencil': return gl.STENCIL_ATTACHMENT
@@ -36,43 +36,58 @@ export interface Renderbuffer {
     readonly id: number
 
     bind: () => void
+    attachFramebuffer: (framebuffer: Framebuffer) => void
     setSize: (width: number, height: number) => void
-
+    reset: () => void
     destroy: () => void
 }
 
-export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferFormat, attachment: RenderbufferAttachment, _width: number, _height: number): Renderbuffer {
-    const { gl, stats } = ctx
-    const _renderbuffer = gl.createRenderbuffer()
-    if (_renderbuffer === null) {
+function getRenderbuffer(gl: GLRenderingContext) {
+    const renderbuffer = gl.createRenderbuffer()
+    if (renderbuffer === null) {
         throw new Error('Could not create WebGL renderbuffer')
     }
+    return renderbuffer
+}
+
+export function createRenderbuffer (gl: GLRenderingContext, format: RenderbufferFormat, attachment: RenderbufferAttachment, _width: number, _height: number): Renderbuffer {
+    let _renderbuffer = getRenderbuffer(gl)
 
     const bind = () => gl.bindRenderbuffer(gl.RENDERBUFFER, _renderbuffer)
-    const _format = getFormat(ctx, format)
-    const _attachment = getAttachment(ctx, attachment)
+    const _format = getFormat(gl, format)
+    const _attachment = getAttachment(gl, attachment)
 
-    bind()
-    gl.renderbufferStorage(gl.RENDERBUFFER, _format, _width, _height)
-    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, _attachment, gl.RENDERBUFFER, _renderbuffer)
+    function init() {
+        bind()
+        gl.renderbufferStorage(gl.RENDERBUFFER, _format, _width, _height)
+    }
+    init()
 
     let destroyed = false
-    stats.renderbufferCount += 1
 
     return {
         id: getNextRenderbufferId(),
 
         bind,
-        setSize: (_width: number, _height: number) => {
+        attachFramebuffer: (framebuffer: Framebuffer) => {
+            framebuffer.bind()
             bind()
-            gl.renderbufferStorage(gl.RENDERBUFFER, _format, _width, _height)
+            gl.framebufferRenderbuffer(gl.FRAMEBUFFER, _attachment, gl.RENDERBUFFER, _renderbuffer)
+            if (isDebugMode) checkFramebufferStatus(gl)
+        },
+        setSize: (width: number, height: number) => {
+            _width = width
+            _height = height
+            init()
+        },
+        reset: () => {
+            _renderbuffer = getRenderbuffer(gl)
+            init()
         },
-
         destroy: () => {
             if (destroyed) return
             gl.deleteRenderbuffer(_renderbuffer)
             destroyed = true
-            stats.framebufferCount -= 1
         }
     }
 }

+ 155 - 0
src/mol-gl/webgl/resources.ts

@@ -0,0 +1,155 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ProgramProps, createProgram, Program } from './program'
+import { ShaderType, createShader, Shader, ShaderProps } from './shader'
+import { GLRenderingContext } from './compat';
+import { Framebuffer, createFramebuffer } from './framebuffer';
+import { WebGLExtensions } from './extensions';
+import { WebGLState } from './state';
+import { AttributeBuffer, UsageHint, ArrayType, AttributeItemSize, createAttributeBuffer, ElementsBuffer, createElementsBuffer, ElementsType, AttributeBuffers } from './buffer';
+import { createReferenceCache, ReferenceItem } from '../../mol-util/reference-cache';
+import { WebGLStats } from './context';
+import { hashString, hashFnv32a } from '../../mol-data/util';
+import { DefineValues, ShaderCode } from '../shader-code';
+import { RenderableSchema } from '../renderable/schema';
+import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer';
+import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture } from './texture';
+import { VertexArray, createVertexArray } from './vertex-array';
+
+function defineValueHash(v: boolean | number | string): number {
+    return typeof v === 'boolean' ? (v ? 1 : 0) :
+        typeof v === 'number' ? v : hashString(v)
+}
+
+function wrapCached<T extends Resource>(resourceItem: ReferenceItem<T>) {
+    const wrapped = {
+        ...resourceItem.value,
+        destroy: () => {
+            resourceItem.free()
+        }
+    }
+
+    return wrapped
+}
+
+//
+
+interface Resource {
+    reset: () => void
+    destroy: () => void
+}
+
+type ResourceName = keyof WebGLStats['resourceCounts']
+
+export interface WebGLResources {
+    attribute: (array: ArrayType, itemSize: AttributeItemSize, divisor: number, usageHint?: UsageHint) => AttributeBuffer
+    elements: (array: ElementsType, usageHint?: UsageHint) => ElementsBuffer
+    framebuffer: () => Framebuffer
+    program: (defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema) => Program
+    renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
+    shader: (type: ShaderType, source: string) => Shader
+    texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
+    vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
+
+    reset: () => void
+    destroy: () => void
+}
+
+export function createResources(gl: GLRenderingContext, state: WebGLState, stats: WebGLStats, extensions: WebGLExtensions): WebGLResources {
+    const sets: { [k in ResourceName]: Set<Resource> } = {
+        attribute: new Set<Resource>(),
+        elements: new Set<Resource>(),
+        framebuffer: new Set<Resource>(),
+        program: new Set<Resource>(),
+        renderbuffer: new Set<Resource>(),
+        shader: new Set<Resource>(),
+        texture: new Set<Resource>(),
+        vertexArray: new Set<Resource>(),
+    }
+
+    function wrap<T extends Resource>(name: ResourceName, resource: T) {
+        sets[name].add(resource)
+        stats.resourceCounts[name] += 1
+        return {
+            ...resource,
+            destroy: () => {
+                resource.destroy()
+                sets[name].delete(resource)
+                stats.resourceCounts[name] -= 1
+            }
+        }
+    }
+
+    const shaderCache = createReferenceCache(
+        (props: ShaderProps) => JSON.stringify(props),
+        (props: ShaderProps) => wrap('shader', createShader(gl, props)),
+        (shader: Shader) => { shader.destroy() }
+    )
+
+    function getShader(type: ShaderType, source: string) {
+        return wrapCached(shaderCache.get({ type, source }))
+    }
+
+    const programCache = createReferenceCache(
+        (props: ProgramProps) => {
+            const array = [ props.shaderCode.id ]
+            Object.keys(props.defineValues).forEach(k => array.push(hashString(k), defineValueHash(props.defineValues[k].ref.value)))
+            return hashFnv32a(array).toString()
+        },
+        (props: ProgramProps) => wrap('program', createProgram(gl, state, extensions, getShader, props)),
+        (program: Program) => { program.destroy() }
+    )
+
+    return {
+        attribute: (array: ArrayType, itemSize: AttributeItemSize, divisor: number, usageHint?: UsageHint) => {
+            return wrap('attribute', createAttributeBuffer(gl, extensions, array, itemSize, divisor, usageHint))
+        },
+        elements: (array: ElementsType, usageHint?: UsageHint) => {
+            return wrap('elements', createElementsBuffer(gl, array, usageHint))
+        },
+        framebuffer: () => {
+            return wrap('framebuffer', createFramebuffer(gl))
+        },
+        program: (defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema) => {
+            return wrapCached(programCache.get({ defineValues, shaderCode, schema }))
+        },
+        renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => {
+            return wrap('renderbuffer', createRenderbuffer(gl, format, attachment, width, height))
+        },
+        shader: getShader,
+        texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => {
+            return wrap('texture', createTexture(gl, extensions, kind, format, type, filter))
+        },
+        vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
+            return wrap('vertexArray', createVertexArray(extensions, program, attributeBuffers, elementsBuffer))
+        },
+
+        reset: () => {
+            sets.attribute.forEach(r => r.reset())
+            sets.elements.forEach(r => r.reset())
+            sets.framebuffer.forEach(r => r.reset())
+            sets.renderbuffer.forEach(r => r.reset())
+            sets.shader.forEach(r => r.reset())
+            sets.program.forEach(r => r.reset())
+            sets.vertexArray.forEach(r => r.reset())
+            sets.texture.forEach(r => r.reset())
+        },
+        destroy: () => {
+            sets.attribute.forEach(r => r.destroy())
+            sets.elements.forEach(r => r.destroy())
+            sets.framebuffer.forEach(r => r.destroy())
+            sets.renderbuffer.forEach(r => r.destroy())
+            sets.shader.forEach(r => r.destroy())
+            sets.program.forEach(r => r.destroy())
+            sets.vertexArray.forEach(r => r.destroy())
+            sets.texture.forEach(r => r.destroy())
+
+            shaderCache.clear()
+            programCache.clear()
+        }
+    }
+}

+ 14 - 15
src/mol-gl/webgl/shader.ts

@@ -1,10 +1,9 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { createReferenceCache, ReferenceCache } from '../../mol-util/reference-cache';
 import { idFactory } from '../../mol-util/id-factory';
 import { GLRenderingContext } from './compat';
 import { isDebugMode } from '../../mol-util/debug';
@@ -20,16 +19,16 @@ function addLineNumbers(source: string) {
 }
 
 export type ShaderType = 'vert' | 'frag'
-export interface ShaderProps { type: ShaderType, source: string }
+export type ShaderProps = { type: ShaderType, source: string }
 export interface Shader {
     readonly id: number
     attach: (program: WebGLProgram) => void
+    reset: () => void
     destroy: () => void
 }
 
-function createShader(gl: GLRenderingContext, props: ShaderProps): Shader {
+function getShader(gl: GLRenderingContext, props: ShaderProps) {
     const { type, source } = props
-
     const shader = gl.createShader(type === 'vert' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER)
     if (shader === null) {
         throw new Error(`Error creating ${type} shader`)
@@ -43,23 +42,23 @@ function createShader(gl: GLRenderingContext, props: ShaderProps): Shader {
         throw new Error(`Error compiling ${type} shader`)
     }
 
+    return shader
+}
+
+export function createShader(gl: GLRenderingContext, props: ShaderProps): Shader {
+    let shader = getShader(gl, props)
+
     return {
         id: getNextShaderId(),
         attach: (program: WebGLProgram) => {
             gl.attachShader(program, shader)
         },
+
+        reset: () => {
+            shader = getShader(gl, props)
+        },
         destroy: () => {
             gl.deleteShader(shader)
         }
     }
-}
-
-export type ShaderCache = ReferenceCache<Shader, ShaderProps>
-
-export function createShaderCache(gl: GLRenderingContext): ShaderCache {
-    return createReferenceCache(
-        (props: ShaderProps) => JSON.stringify(props),
-        (props: ShaderProps) => createShader(gl, props),
-        (shader: Shader) => { shader.destroy() }
-    )
 }

+ 35 - 13
src/mol-gl/webgl/state.ts

@@ -58,10 +58,12 @@ export type WebGLState = {
     blendEquation: (mode: number) => void
     /** set the RGB blend equation and alpha blend equation separately, determines how a new pixel is combined with an existing */
     blendEquationSeparate: (modeRGB: number, modeAlpha: number) => void
+
+    reset: () => void
 }
 
 export function createState(gl: GLRenderingContext): WebGLState {
-    const enabledCapabilities: { [k: number]: boolean } = {}
+    let enabledCapabilities: { [k: number]: boolean } = {}
 
     let currentFrontFace = gl.getParameter(gl.FRONT_FACE)
     let currentCullFace = gl.getParameter(gl.CULL_FACE_MODE)
@@ -114,20 +116,22 @@ export function createState(gl: GLRenderingContext): WebGLState {
             }
         },
         colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => {
-            if (red !== currentColorMask[0] || green !== currentColorMask[1] || blue !== currentColorMask[2] || alpha !== currentColorMask[3])
-            gl.colorMask(red, green, blue, alpha)
-            currentColorMask[0] = red
-            currentColorMask[1] = green
-            currentColorMask[2] = blue
-            currentColorMask[3] = alpha
+            if (red !== currentColorMask[0] || green !== currentColorMask[1] || blue !== currentColorMask[2] || alpha !== currentColorMask[3]) {
+                gl.colorMask(red, green, blue, alpha)
+                currentColorMask[0] = red
+                currentColorMask[1] = green
+                currentColorMask[2] = blue
+                currentColorMask[3] = alpha
+            }
         },
         clearColor: (red: number, green: number, blue: number, alpha: number) => {
-            if (red !== currentClearColor[0] || green !== currentClearColor[1] || blue !== currentClearColor[2] || alpha !== currentClearColor[3])
-            gl.clearColor(red, green, blue, alpha)
-            currentClearColor[0] = red
-            currentClearColor[1] = green
-            currentClearColor[2] = blue
-            currentClearColor[3] = alpha
+            if (red !== currentClearColor[0] || green !== currentClearColor[1] || blue !== currentClearColor[2] || alpha !== currentClearColor[3]) {
+                gl.clearColor(red, green, blue, alpha)
+                currentClearColor[0] = red
+                currentClearColor[1] = green
+                currentClearColor[2] = blue
+                currentClearColor[3] = alpha
+            }
         },
 
         blendFunc: (src: number, dst: number) => {
@@ -162,6 +166,24 @@ export function createState(gl: GLRenderingContext): WebGLState {
                 currentBlendEqRGB = modeRGB
                 currentBlendEqAlpha = modeAlpha
             }
+        },
+
+        reset: () => {
+            enabledCapabilities = {}
+
+            currentFrontFace = gl.getParameter(gl.FRONT_FACE)
+            currentCullFace = gl.getParameter(gl.CULL_FACE_MODE)
+            currentDepthMask = gl.getParameter(gl.DEPTH_WRITEMASK)
+            currentColorMask = gl.getParameter(gl.COLOR_WRITEMASK)
+            currentClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE)
+
+            currentBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB)
+            currentBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB)
+            currentBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA)
+            currentBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA)
+
+            currentBlendEqRGB = gl.getParameter(gl.BLEND_EQUATION_RGB)
+            currentBlendEqAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA)
         }
     }
 }

+ 120 - 81
src/mol-gl/webgl/texture.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,8 +10,9 @@ import { ValueCell } from '../../mol-util';
 import { RenderableSchema } from '../renderable/schema';
 import { idFactory } from '../../mol-util/id-factory';
 import { Framebuffer } from './framebuffer';
-import { isWebGL2 } from './compat';
+import { isWebGL2, GLRenderingContext } from './compat';
 import { ValueOf } from '../../mol-util/type-helpers';
+import { WebGLExtensions } from './extensions';
 
 const getNextTextureId = idFactory()
 
@@ -31,8 +32,7 @@ export type TextureFormat = 'alpha' | 'rgb' | 'rgba' | 'depth'
 export type TextureAttachment = 'depth' | 'stencil' | 'color0' | 'color1' | 'color2' | 'color3' | 'color4' | 'color5' | 'color6' | 'color7' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
 export type TextureFilter = 'nearest' | 'linear'
 
-export function getTarget(ctx: WebGLContext, kind: TextureKind): number {
-    const { gl } = ctx
+export function getTarget(gl: GLRenderingContext, kind: TextureKind): number {
     switch (kind) {
         case 'image-uint8': return gl.TEXTURE_2D
         case 'image-float32': return gl.TEXTURE_2D
@@ -47,8 +47,7 @@ export function getTarget(ctx: WebGLContext, kind: TextureKind): number {
     throw new Error(`unknown texture kind '${kind}'`)
 }
 
-export function getFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number {
-    const { gl } = ctx
+export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: TextureType): number {
     switch (format) {
         case 'alpha':
             if (isWebGL2(gl) && type === 'float') return gl.RED
@@ -59,8 +58,7 @@ export function getFormat(ctx: WebGLContext, format: TextureFormat, type: Textur
     }
 }
 
-export function getInternalFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number {
-    const { gl } = ctx
+export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat, type: TextureType): number {
     if (isWebGL2(gl)) {
         switch (format) {
             case 'alpha':
@@ -82,11 +80,10 @@ export function getInternalFormat(ctx: WebGLContext, format: TextureFormat, type
                 return gl.DEPTH_COMPONENT16
         }
     }
-    return getFormat(ctx, format, type)
+    return getFormat(gl, format, type)
 }
 
-export function getType(ctx: WebGLContext, type: TextureType): number {
-    const { gl } = ctx
+export function getType(gl: GLRenderingContext, type: TextureType): number {
     switch (type) {
         case 'ubyte': return gl.UNSIGNED_BYTE
         case 'ushort': return gl.UNSIGNED_SHORT
@@ -94,16 +91,14 @@ export function getType(ctx: WebGLContext, type: TextureType): number {
     }
 }
 
-export function getFilter(ctx: WebGLContext, type: TextureFilter): number {
-    const { gl } = ctx
+export function getFilter(gl: GLRenderingContext, type: TextureFilter): number {
     switch (type) {
         case 'nearest': return gl.NEAREST
         case 'linear': return gl.LINEAR
     }
 }
 
-export function getAttachment(ctx: WebGLContext, attachment: TextureAttachment): number {
-    const { gl, extensions } = ctx
+export function getAttachment(gl: GLRenderingContext, extensions: WebGLExtensions, attachment: TextureAttachment): number {
     switch (attachment) {
         case 'depth': return gl.DEPTH_ATTACHMENT
         case 'stencil': return gl.STENCIL_ATTACHMENT
@@ -130,9 +125,9 @@ export interface Texture {
     readonly internalFormat: number
     readonly type: number
 
-    readonly width: number
-    readonly height: number
-    readonly depth: number
+    getWidth: () => number
+    getHeight: () => number
+    getDepth: () => number
 
     define: (width: number, height: number, depth?: number) => void
     load: (image: TextureImage<any> | TextureVolume<any>) => void
@@ -141,6 +136,8 @@ export interface Texture {
     /** Use `layer` to attach a z-slice of a 3D texture */
     attachFramebuffer: (framebuffer: Framebuffer, attachment: TextureAttachment, layer?: number) => void
     detachFramebuffer: (framebuffer: Framebuffer, attachment: TextureAttachment) => void
+
+    reset: () => void
     destroy: () => void
 }
 
@@ -149,13 +146,23 @@ export type TextureId = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 1
 export type TextureValues = { [k: string]: ValueCell<TextureValueType> }
 export type Textures = [string, Texture][]
 
-export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: TextureFormat, _type: TextureType, _filter: TextureFilter): Texture {
-    const id = getNextTextureId()
-    const { gl, stats } = ctx
+type FramebufferAttachment = {
+    framebuffer: Framebuffer
+    attachment: TextureAttachment
+    layer?: number
+}
+
+function getTexture(gl: GLRenderingContext) {
     const texture = gl.createTexture()
     if (texture === null) {
         throw new Error('Could not create WebGL texture')
     }
+    return texture
+}
+// export type TextureProps = { kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter }
+export function createTexture(gl: GLRenderingContext, extensions: WebGLExtensions, kind: TextureKind, _format: TextureFormat, _type: TextureType, _filter: TextureFilter): Texture {
+    const id = getNextTextureId()
+    let texture = getTexture(gl)
 
     // check texture kind and type compatability
     if (
@@ -166,24 +173,77 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
         throw new Error(`texture kind '${kind}' and type '${_type}' are incompatible`)
     }
 
-    const target = getTarget(ctx, kind)
-    const filter = getFilter(ctx, _filter)
-    const format = getFormat(ctx, _format, _type)
-    const internalFormat = getInternalFormat(ctx, _format, _type)
-    const type = getType(ctx, _type)
+    const target = getTarget(gl, kind)
+    const filter = getFilter(gl, _filter)
+    const format = getFormat(gl, _format, _type)
+    const internalFormat = getInternalFormat(gl, _format, _type)
+    const type = getType(gl, _type)
 
-    gl.bindTexture(target, texture)
-    gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter)
-    gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter)
-    // clamp-to-edge needed for non-power-of-two textures in webgl
-    gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
-    gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
-    gl.bindTexture(target, null)
+    function init() {
+        gl.bindTexture(target, texture)
+        gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter)
+        gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter)
+        // clamp-to-edge needed for non-power-of-two textures in webgl
+        gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
+        gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
+        gl.bindTexture(target, null)
+    }
+    init()
 
-    let width = 0, height = 0, depth = 0
+    let fba: undefined | FramebufferAttachment = undefined
 
+    let width = 0, height = 0, depth = 0
+    let loadedData: undefined | TextureImage<any> | TextureVolume<any>
     let destroyed = false
-    stats.textureCount += 1
+
+    function define(_width: number, _height: number, _depth?: number) {
+        width = _width, height = _height, depth = _depth || 0
+        gl.bindTexture(target, texture)
+        if (target === gl.TEXTURE_2D) {
+            gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null)
+        } else if (isWebGL2(gl) && target === gl.TEXTURE_3D && depth !== undefined) {
+            gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null)
+        } else {
+            throw new Error('unknown texture target')
+        }
+    }
+
+    function load(data: TextureImage<any> | TextureVolume<any>) {
+        gl.bindTexture(target, texture)
+        // unpack alignment of 1 since we use textures only for data
+        gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
+        gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
+        gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
+        if (target === gl.TEXTURE_2D) {
+            const { array, width: _width, height: _height } = data as TextureImage<any>
+            width = _width, height = _height;
+            gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
+        } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
+            const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any>
+            width = _width, height = _height, depth = _depth
+            gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array)
+        } else {
+            throw new Error('unknown texture target')
+        }
+        gl.bindTexture(target, null)
+        loadedData = data
+    }
+
+    function attachFramebuffer(framebuffer: Framebuffer, attachment: TextureAttachment, layer?: number) {
+        if (fba && fba.framebuffer === framebuffer && fba.attachment === attachment && fba.layer === layer) {
+            return
+        }
+        framebuffer.bind()
+        if (target === gl.TEXTURE_2D) {
+            gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), gl.TEXTURE_2D, texture, 0)
+        } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
+            if (layer === undefined) throw new Error('need `layer` to attach 3D texture')
+            gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), texture, 0, layer)
+        } else {
+            throw new Error('unknown texture target')
+        }
+        fba = { framebuffer, attachment, layer }
+    }
 
     return {
         id,
@@ -192,40 +252,12 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
         internalFormat,
         type,
 
-        get width () { return width },
-        get height () { return height },
-        get depth () { return depth },
+        getWidth: () => width,
+        getHeight: () => height,
+        getDepth: () => depth,
 
-        define: (_width: number, _height: number, _depth?: number) => {
-            width = _width, height = _height, depth = _depth || 0
-            gl.bindTexture(target, texture)
-            if (target === gl.TEXTURE_2D) {
-                gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null)
-            } else if (isWebGL2(gl) && target === gl.TEXTURE_3D && depth !== undefined) {
-                gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null)
-            } else {
-                throw new Error('unknown texture target')
-            }
-        },
-        load: (data: TextureImage<any> | TextureVolume<any>) => {
-            gl.bindTexture(target, texture)
-            // unpack alignment of 1 since we use textures only for data
-            gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
-            gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
-            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
-            if (target === gl.TEXTURE_2D) {
-                const { array, width: _width, height: _height } = data as TextureImage<any>
-                width = _width, height = _height;
-                gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
-            } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
-                const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any>
-                width = _width, height = _height, depth = _depth
-                gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array)
-            } else {
-                throw new Error('unknown texture target')
-            }
-            gl.bindTexture(target, null)
-        },
+        define,
+        load,
         bind: (id: TextureId) => {
             gl.activeTexture(gl.TEXTURE0 + id)
             gl.bindTexture(target, texture)
@@ -234,37 +266,44 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
             gl.activeTexture(gl.TEXTURE0 + id)
             gl.bindTexture(target, null)
         },
-        attachFramebuffer: (framebuffer: Framebuffer, attachment: TextureAttachment, layer?: number) => {
+        attachFramebuffer,
+        detachFramebuffer: (framebuffer: Framebuffer, attachment: TextureAttachment) => {
             framebuffer.bind()
             if (target === gl.TEXTURE_2D) {
-                gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(ctx, attachment), gl.TEXTURE_2D, texture, 0)
+                gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), gl.TEXTURE_2D, null, 0)
             } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
-                if (layer === undefined) throw new Error('need `layer` to attach 3D texture')
-                gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(ctx, attachment), texture, 0, layer)
+                gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), null, 0, 0)
             } else {
                 throw new Error('unknown texture target')
             }
+            fba = undefined
         },
-        detachFramebuffer: (framebuffer: Framebuffer, attachment: TextureAttachment) => {
-            framebuffer.bind()
-            if (target === gl.TEXTURE_2D) {
-                gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(ctx, attachment), gl.TEXTURE_2D, null, 0)
-            } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
-                gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(ctx, attachment), null, 0, 0)
+        reset: () => {
+            texture = getTexture(gl)
+            init()
+
+            if (loadedData) {
+                load(loadedData)
             } else {
-                throw new Error('unknown texture target')
+                define(width, height, depth)
+            }
+
+            if (fba) {
+                // TODO unclear why calling `attachFramebuffer` here does not work reliably after context loss
+                // e.g. it still needs to be called in `DrawPass` to work
+                fba = undefined
             }
         },
         destroy: () => {
             if (destroyed) return
             gl.deleteTexture(texture)
             destroyed = true
-            stats.textureCount -= 1
         }
     }
 }
 
 export function createTextures(ctx: WebGLContext, schema: RenderableSchema, values: TextureValues) {
+    const { resources } = ctx
     const textures: Textures = []
     Object.keys(schema).forEach(k => {
         const spec = schema[k]
@@ -272,7 +311,7 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
             if (spec.kind === 'texture') {
                 textures[textures.length] = [k, values[k].ref.value as Texture]
             } else {
-                const texture = createTexture(ctx, spec.kind, spec.format, spec.dataType, spec.filter)
+                const texture = resources.texture(spec.kind, spec.format, spec.dataType, spec.filter)
                 texture.load(values[k].ref.value as TextureImage<any> | TextureVolume<any>)
                 textures[textures.length] = [k, texture]
             }

+ 55 - 22
src/mol-gl/webgl/vertex-array.ts

@@ -1,42 +1,75 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { WebGLContext } from './context';
 import { Program } from './program';
 import { ElementsBuffer, AttributeBuffers } from './buffer';
+import { WebGLExtensions } from './extensions';
+import { idFactory } from '../../mol-util/id-factory';
 
-export function createVertexArray(ctx: WebGLContext, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) {
-    const { vertexArrayObject } = ctx.extensions
-    let vertexArray: WebGLVertexArrayObject | null = null
-    if (vertexArrayObject) {
-        vertexArray = vertexArrayObject.createVertexArray()
-        if (vertexArray) {
-            updateVertexArray(ctx, vertexArray, program, attributeBuffers, elementsBuffer)
-            ctx.stats.vaoCount += 1
-        } else {
-            console.warn('Could not create WebGL vertex array')
-        }
+const getNextVertexArrayId = idFactory()
+
+function getVertexArray(extensions: WebGLExtensions): WebGLVertexArrayObject {
+    const { vertexArrayObject } = extensions
+    if (!vertexArrayObject) {
+        throw new Error('VertexArrayObject not supported')
+    }
+    const vertexArray = vertexArrayObject.createVertexArray()
+    if (!vertexArray) {
+        throw new Error('Could not create WebGL vertex array')
     }
     return vertexArray
 }
 
-export function updateVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) {
-    const { vertexArrayObject } = ctx.extensions
-    if (vertexArrayObject && vertexArray) {
+function getVertexArrayObject(extensions: WebGLExtensions) {
+    const { vertexArrayObject } = extensions
+    if (vertexArrayObject === null) {
+        throw new Error('VertexArrayObject not supported')
+    }
+    return vertexArrayObject
+}
+
+export interface VertexArray {
+    readonly id: number
+
+    bind: () => void
+    update: () => void
+    reset: () => void
+    destroy: () => void
+}
+
+export function createVertexArray(extensions: WebGLExtensions, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer): VertexArray {
+    const id = getNextVertexArrayId()
+    let vertexArray = getVertexArray(extensions)
+    let vertexArrayObject = getVertexArrayObject(extensions)
+
+    function update() {
         vertexArrayObject.bindVertexArray(vertexArray)
         if (elementsBuffer) elementsBuffer.bind()
         program.bindAttributes(attributeBuffers)
         vertexArrayObject.bindVertexArray(null)
     }
-}
 
-export function deleteVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null) {
-    const { vertexArrayObject } = ctx.extensions
-    if (vertexArrayObject && vertexArray) {
-        vertexArrayObject.deleteVertexArray(vertexArray)
-        ctx.stats.vaoCount -= 1
+    update()
+    let destroyed = false
+
+    return {
+        id,
+        bind: () => {
+            vertexArrayObject.bindVertexArray(vertexArray)
+        },
+        update,
+        reset: () => {
+            vertexArray = getVertexArray(extensions)
+            vertexArrayObject = getVertexArrayObject(extensions)
+            update()
+        },
+        destroy: () => {
+            if (destroyed) return
+            vertexArrayObject.deleteVertexArray(vertexArray)
+            destroyed = true
+        }
     }
 }

+ 14 - 1
src/mol-io/common/binary.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -16,4 +16,17 @@ export function flipByteOrder(data: Uint8Array, bytes: number) {
         }
     }
     return buffer;
+}
+
+const ChunkSize = 0x7000
+export function uint8ToString(array: Uint8Array) {
+    if (array.length > ChunkSize) {
+        const c = []
+        for (let i = 0; i < array.length; i += ChunkSize) {
+            c.push(String.fromCharCode.apply(null, array.subarray(i, i + ChunkSize)))
+        }
+        return c.join('')
+    } else {
+        return String.fromCharCode.apply(null, array)
+    }
 }

+ 12 - 2
src/mol-io/common/utf8.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * Adapted from https://github.com/rcsb/mmtf-javascript
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -53,7 +53,7 @@ function throwError(err: string) {
     throw new Error(err);
 }
 
-export function utf8Read(data: Uint8Array, offset: number, length: number) {
+function _utf8Read(data: Uint8Array, offset: number, length: number) {
     let chars = __chars;
     let str: string[] | undefined = void 0, chunk: string[] = [], chunkSize = 512, chunkOffset = 0;
 
@@ -98,6 +98,16 @@ export function utf8Read(data: Uint8Array, offset: number, length: number) {
     return str.join('');
 }
 
+const utf8Decoder = (typeof TextDecoder !== 'undefined') ? new TextDecoder() : undefined
+export function utf8Read(data: Uint8Array, offset: number, length: number) {
+    if (utf8Decoder) {
+        const input = (offset || length !== data.length) ? data.subarray(offset, offset + length) : data
+        return utf8Decoder.decode(input)
+    } else {
+        return _utf8Read(data, offset, length)
+    }
+}
+
 export function utf8ByteCount(str: string) {
     let count = 0;
     for (let i = 0, l = str.length; i < l; i++) {

+ 25 - 7
src/mol-io/reader/_spec/ccp4.spec.ts

@@ -6,19 +6,37 @@
 
 import * as CCP4 from '../ccp4/parser'
 
-const ccp4Buffer = new Uint8Array(4 * 64)
+function createCcp4Data() {
+    const data = new Uint8Array(4 * 256 + 6)
+
+    const dv = new DataView(data.buffer)
+
+    dv.setInt8(52 * 4, 'M'.charCodeAt(0))
+    dv.setInt8(52 * 4 + 1, 'A'.charCodeAt(0))
+    dv.setInt8(52 * 4 + 2, 'P'.charCodeAt(0))
+    dv.setInt8(52 * 4 + 3, ' '.charCodeAt(0))
+
+    dv.setInt32(0 * 4, 1) // NC
+    dv.setInt32(1 * 4, 2) // NR
+    dv.setInt32(2 * 4, 3) // NS
+
+    return data
+}
 
 describe('ccp4 reader', () => {
     it('basic', async () => {
-        const parsed = await CCP4.parse(ccp4Buffer).run();
+        const data = createCcp4Data()
+        const parsed = await CCP4.parse(data).run();
 
         if (parsed.isError) {
-            console.log(parsed)
-            return;
+            throw new Error(parsed.message)
         }
-        // const ccp4File = parsed.result;
-        // const { header, values } = ccp4File;
 
-        // TODO
+        const ccp4File = parsed.result;
+        const { header } = ccp4File;
+
+        expect(header.NC).toBe(1)
+        expect(header.NR).toBe(2)
+        expect(header.NS).toBe(3)
     });
 });

+ 17 - 2
src/mol-io/reader/_spec/common.spec.ts

@@ -1,10 +1,10 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { parseFloat as fastParseFloat, parseInt as fastParseInt } from '../../../mol-io/reader/common/text/number-parser';
+import { parseFloat as fastParseFloat, parseInt as fastParseInt, getNumberType, NumberType } from '../../../mol-io/reader/common/text/number-parser';
 
 describe('common', () => {
     it('number-parser fastParseFloat', () => {
@@ -14,4 +14,19 @@ describe('common', () => {
     it('number-parser fastParseInt', () => {
         expect(fastParseInt('11(23)', 0, 11)).toBe(11)
     });
+
+    it('number-parser getNumberType', () => {
+        expect(getNumberType('11')).toBe(NumberType.Int)
+        expect(getNumberType('5E93')).toBe(NumberType.Scientific)
+        expect(getNumberType('0.42')).toBe(NumberType.Float)
+        expect(getNumberType('Foo123')).toBe(NumberType.NaN)
+        expect(getNumberType('11.0829(23)')).toBe(NumberType.NaN)
+        expect(getNumberType('1..2')).toBe(NumberType.NaN)
+        expect(getNumberType('.')).toBe(NumberType.NaN)
+        expect(getNumberType('-.')).toBe(NumberType.NaN)
+        expect(getNumberType('e')).toBe(NumberType.NaN)
+        expect(getNumberType('-e')).toBe(NumberType.NaN)
+        expect(getNumberType('1e')).toBe(NumberType.Scientific)
+        expect(getNumberType('-1e')).toBe(NumberType.Scientific)
+    });
 });

+ 74 - 0
src/mol-io/reader/_spec/dcd.spec.ts

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { parseDcd } from '../dcd/parser';
+
+function createDcdData() {
+    const data = new Uint8Array(4 * 128)
+
+    const dv = new DataView(data.buffer)
+
+    // set little endian
+    dv.setInt32(0, 84)
+
+    // set format string
+    dv.setUint8(4, 'D'.charCodeAt(0))
+    dv.setUint8(5, 'R'.charCodeAt(0))
+    dv.setUint8(6, 'O'.charCodeAt(0))
+    dv.setUint8(7, 'C'.charCodeAt(0))
+
+    dv.setInt32(8, 1) // NSET
+
+    // header end
+    dv.setInt32(22 * 4, 84)
+
+    // title
+    const titleEnd = 164
+    const titleStart = 23 * 4 + 1
+    dv.setInt32(23 * 4, titleEnd)
+    dv.setInt32(titleStart + titleEnd + 4 - 1, titleEnd)
+
+    // atoms
+    const atomStart = 23 * 4 + titleEnd + 8
+    dv.setInt32(atomStart, 4)
+    dv.setInt32(atomStart + 4, 1) // one atom
+    dv.setInt32(atomStart + 8, 4)
+
+    // coords
+    const coordsStart = atomStart + 12
+    dv.setInt32(coordsStart, 4)
+    dv.setFloat32(coordsStart + 4, 0.1)
+    dv.setInt32(coordsStart + 8, 4)
+    dv.setInt32(coordsStart + 12, 4)
+    dv.setFloat32(coordsStart + 16, 0.2)
+    dv.setInt32(coordsStart + 20, 4)
+    dv.setInt32(coordsStart + 24, 4)
+    dv.setFloat32(coordsStart + 28, 0.3)
+    dv.setInt32(coordsStart + 32, 4)
+
+    return data
+}
+
+describe('dcd reader', () => {
+    it('basic', async () => {
+        const data = createDcdData()
+        const parsed = await parseDcd(data).run();
+
+        if (parsed.isError) {
+            throw new Error(parsed.message)
+        }
+
+        const dcdFile = parsed.result;
+        const { header, frames } = dcdFile;
+
+        expect(header.NSET).toBe(1)
+        expect(header.NATOM).toBe(1)
+
+        expect(frames[0].x[0]).toBeCloseTo(0.1, 0.0001);
+        expect(frames[0].y[0]).toBeCloseTo(0.2, 0.0001);
+        expect(frames[0].z[0]).toBeCloseTo(0.3, 0.0001);
+    });
+});

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

@@ -109,7 +109,6 @@ end_header
 255 0 255
 `
 
-
 describe('ply reader', () => {
     it('basic', async () => {
         const parsed = await Ply(plyString).run();

+ 110 - 0
src/mol-io/reader/_spec/psf.spec.ts

@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { parsePsf } from '../psf/parser';
+
+const psfString = `PSF CMAP CHEQ
+
+       2 !NTITLE
+* BETA HARPIN IN IMPLICIT SOLVENT
+*  DATE:    11/22/10     16:54: 9      CREATED BY USER: aokur
+
+      42 !NATOM
+       1 ALA3 1    ALA  CAY    24  -0.270000       12.0110           0   0.00000     -0.301140E-02
+       2 ALA3 1    ALA  HY1     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+       3 ALA3 1    ALA  HY2     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+       4 ALA3 1    ALA  HY3     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+       5 ALA3 1    ALA  CY     20   0.510000       12.0110           0   0.00000     -0.301140E-02
+       6 ALA3 1    ALA  OY     70  -0.510000       15.9990           0   0.00000     -0.301140E-02
+       7 ALA3 1    ALA  N      54  -0.470000       14.0070           0   0.00000     -0.301140E-02
+       8 ALA3 1    ALA  HN      1   0.310000       1.00800           0   0.00000     -0.301140E-02
+       9 ALA3 1    ALA  CA     22   0.700000E-01   12.0110           0   0.00000     -0.301140E-02
+      10 ALA3 1    ALA  HA      6   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      11 ALA3 1    ALA  CB     24  -0.270000       12.0110           0   0.00000     -0.301140E-02
+      12 ALA3 1    ALA  HB1     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      13 ALA3 1    ALA  HB2     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      14 ALA3 1    ALA  HB3     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      15 ALA3 1    ALA  C      20   0.510000       12.0110           0   0.00000     -0.301140E-02
+      16 ALA3 1    ALA  O      70  -0.510000       15.9990           0   0.00000     -0.301140E-02
+      17 ALA3 2    ALA  N      54  -0.470000       14.0070           0   0.00000     -0.301140E-02
+      18 ALA3 2    ALA  HN      1   0.310000       1.00800           0   0.00000     -0.301140E-02
+      19 ALA3 2    ALA  CA     22   0.700000E-01   12.0110           0   0.00000     -0.301140E-02
+      20 ALA3 2    ALA  HA      6   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      21 ALA3 2    ALA  CB     24  -0.270000       12.0110           0   0.00000     -0.301140E-02
+      22 ALA3 2    ALA  HB1     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      23 ALA3 2    ALA  HB2     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      24 ALA3 2    ALA  HB3     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      25 ALA3 2    ALA  C      20   0.510000       12.0110           0   0.00000     -0.301140E-02
+      26 ALA3 2    ALA  O      70  -0.510000       15.9990           0   0.00000     -0.301140E-02
+      27 ALA3 3    ALA  N      54  -0.470000       14.0070           0   0.00000     -0.301140E-02
+      28 ALA3 3    ALA  HN      1   0.310000       1.00800           0   0.00000     -0.301140E-02
+      29 ALA3 3    ALA  CA     22   0.700000E-01   12.0110           0   0.00000     -0.301140E-02
+      30 ALA3 3    ALA  HA      6   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      31 ALA3 3    ALA  CB     24  -0.270000       12.0110           0   0.00000     -0.301140E-02
+      32 ALA3 3    ALA  HB1     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      33 ALA3 3    ALA  HB2     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      34 ALA3 3    ALA  HB3     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      35 ALA3 3    ALA  C      20   0.510000       12.0110           0   0.00000     -0.301140E-02
+      36 ALA3 3    ALA  O      70  -0.510000       15.9990           0   0.00000     -0.301140E-02
+      37 ALA3 3    ALA  NT     54  -0.470000       14.0070           0   0.00000     -0.301140E-02
+      38 ALA3 3    ALA  HNT     1   0.310000       1.00800           0   0.00000     -0.301140E-02
+      39 ALA3 3    ALA  CAT    24  -0.110000       12.0110           0   0.00000     -0.301140E-02
+      40 ALA3 3    ALA  HT1     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      41 ALA3 3    ALA  HT2     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+      42 ALA3 3    ALA  HT3     3   0.900000E-01   1.00800           0   0.00000     -0.301140E-02
+
+      41 !NBOND: bonds
+       5       1       5       7       1       2       1       3
+       1       4       6       5      11       9       7       8
+       7       9      15       9      15      17       9      10
+      11      12      11      13      11      14      16      15
+      21      19      17      18      17      19      25      19
+      25      27      19      20      21      22      21      23
+      21      24      26      25      31      29      27      28
+      27      29      35      29      29      30      31      32
+      31      33      31      34      36      35      35      37
+      37      38      37      39      39      40      39      41
+      39      42
+`
+
+describe('psf reader', () => {
+    it('basic', async () => {
+        const parsed = await parsePsf(psfString).run();
+
+        if (parsed.isError) {
+            throw new Error(parsed.message)
+        }
+
+        const psfFile = parsed.result;
+        const { id, title, atoms, bonds } = psfFile;
+
+        expect(id).toBe('PSF CMAP CHEQ')
+        expect(title).toEqual([
+            'BETA HARPIN IN IMPLICIT SOLVENT',
+            'DATE:    11/22/10     16:54: 9      CREATED BY USER: aokur'
+        ])
+
+        expect(atoms.atomId.value(0)).toBe(1)
+        expect(atoms.atomId.value(41)).toBe(42)
+        expect(atoms.segmentName.value(0)).toBe('ALA3')
+        expect(atoms.residueId.value(0)).toBe(1)
+        expect(atoms.residueId.value(41)).toBe(3)
+        expect(atoms.residueName.value(0)).toBe('ALA')
+        expect(atoms.atomName.value(0)).toBe('CAY')
+        expect(atoms.atomName.value(41)).toBe('HT3')
+        expect(atoms.atomType.value(0)).toBe('24')
+        expect(atoms.atomType.value(41)).toBe('3')
+        expect(atoms.charge.value(0)).toBeCloseTo(-0.270000, 0.00001)
+        expect(atoms.charge.value(41)).toBeCloseTo(0.090000, 0.00001)
+        expect(atoms.mass.value(0)).toBeCloseTo(12.0110, 0.00001)
+        expect(atoms.mass.value(41)).toBeCloseTo(1.00800, 0.00001)
+
+        expect(bonds.atomIdA.value(0)).toBe(5)
+        expect(bonds.atomIdB.value(0)).toBe(1)
+        expect(bonds.atomIdA.value(40)).toBe(39)
+        expect(bonds.atomIdB.value(40)).toBe(42)
+    });
+});

+ 6 - 5
src/mol-io/reader/cif/data-model.ts

@@ -222,7 +222,7 @@ export namespace CifField {
     }
 
     export function ofColumn(column: Column<any>): CifField {
-        const { rowCount, valueKind, areValuesEqual } = column;
+        const { rowCount, valueKind, areValuesEqual, isDefined } = column;
 
         let str: CifField['str']
         let int: CifField['int']
@@ -253,7 +253,7 @@ export namespace CifField {
         return {
             __array: void 0,
             binaryEncoding: void 0,
-            isDefined: true,
+            isDefined,
             rowCount,
             str,
             int,
@@ -300,7 +300,7 @@ export function getTensor(category: CifCategory, field: string, space: Tensor.Sp
 }
 
 export function getCifFieldType(field: CifField): Column.Schema.Int | Column.Schema.Float | Column.Schema.Str {
-    let floatCount = 0, hasString = false, undefinedCount = 0;
+    let floatCount = 0, hasStringOrScientific = false, undefinedCount = 0;
     for (let i = 0, _i = field.rowCount; i < _i; i++) {
         const k = field.valueKind(i);
         if (k !== Column.ValueKind.Present) {
@@ -310,10 +310,11 @@ export function getCifFieldType(field: CifField): Column.Schema.Int | Column.Sch
         const type = getNumberType(field.str(i));
         if (type === NumberType.Int) continue;
         else if (type === NumberType.Float) floatCount++;
-        else { hasString = true; break; }
+        else { hasStringOrScientific = true; break; }
     }
 
-    if (hasString || undefinedCount === field.rowCount) return Column.Schema.str;
+    // numbers in scientific notation and plain text are not distinguishable
+    if (hasStringOrScientific || undefinedCount === field.rowCount) return Column.Schema.str;
     if (floatCount > 0) return Column.Schema.float;
     return Column.Schema.int;
 }

+ 5 - 5
src/mol-io/reader/common/text/column/token.ts

@@ -19,11 +19,11 @@ export function TokenColumn<T extends Column.Schema>(tokens: Tokens, schema: T):
     const { valueType: type } = schema;
 
     const value: Column<T['T']>['value'] =
-          type === 'str'
-        ? row => data.substring(indices[2 * row], indices[2 * row + 1])
-        : type === 'int'
-        ? row => fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0
-        : row => fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
+        type === 'str'
+            ? row => data.substring(indices[2 * row], indices[2 * row + 1])
+            : type === 'int'
+                ? row => fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0
+                : row => fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
 
     return {
         schema: schema,

+ 16 - 4
src/mol-io/reader/common/text/number-parser.ts

@@ -1,8 +1,10 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * from https://github.com/dsehnal/CIFTools.js
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * based in part on https://github.com/dsehnal/CIFTools.js
  */
 
 /**
@@ -78,6 +80,7 @@ export function parseFloat(str: string, start: number, end: number) {
 export const enum NumberType {
     Int,
     Float,
+    Scientific,
     NaN
 }
 
@@ -94,16 +97,22 @@ function isInt(str: string, start: number, end: number) {
 function getNumberTypeScientific(str: string, start: number, end: number) {
     // handle + in '1e+1' separately.
     if (str.charCodeAt(start) === 43 /* + */) start++;
-    return isInt(str, start, end) ? NumberType.Float : NumberType.NaN;
+    return isInt(str, start, end) ? NumberType.Scientific : NumberType.NaN;
 }
 
 /** The whole range must match, otherwise returns NaN */
 export function getNumberType(str: string): NumberType {
     let start = 0, end = str.length;
-    if (str.charCodeAt(start) === 45) {
+
+    if (str.charCodeAt(start) === 45) { // -
         ++start;
     }
 
+    // string is . or -.
+    if (str.charCodeAt(start) === 46 && end - start === 1) {
+        return NumberType.NaN
+    }
+
     while (start < end) {
         let c = str.charCodeAt(start) - 48;
         if (c >= 0 && c < 10) {
@@ -124,6 +133,9 @@ export function getNumberType(str: string): NumberType {
             }
             return hasDigit ? NumberType.Float : NumberType.Int;
         } else if (c === 53 || c === 21) { // 'e'/'E'
+            if (start === 0 || start === 1 && str.charCodeAt(0) === 45) {
+                return NumberType.NaN; // string starts with e/E or -e/-E
+            }
             return getNumberTypeScientific(str, start + 1, end);
         }
         else break;

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

@@ -91,7 +91,7 @@ namespace Tokenizer {
         return eatLine(state);
     }
 
-    /** Advance the state by the given number of lines and return line starts/ends as tokens. */
+    /** Advance the state by the given number of lines and return line as string. */
     export function readLine(state: Tokenizer): string {
         markLine(state);
         return getTokenString(state);
@@ -186,7 +186,7 @@ namespace Tokenizer {
      * Handles incrementing line count.
      */
     export function skipWhitespace(state: Tokenizer): number {
-        let prev = 10;
+        let prev = -1;
         while (state.position < state.length) {
             let c = state.data.charCodeAt(state.position);
             switch (c) {

+ 215 - 0
src/mol-io/reader/dcd/parser.ts

@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ReaderResult as Result } from '../result'
+import { Task } from '../../../mol-task'
+import { Cell } from '../../../mol-math/geometry/spacegroup/cell'
+import { Vec3 } from '../../../mol-math/linear-algebra'
+import { Mutable } from '../../../mol-util/type-helpers'
+import { uint8ToString } from '../../common/binary'
+
+export interface DcdHeader {
+    readonly NSET: number,
+    readonly ISTART: number,
+    readonly NSAVC: number,
+    readonly NAMNF: number,
+    readonly DELTA: number,
+    readonly TITLE: string,
+    readonly NATOM: number
+}
+
+export interface DcdFrame {
+    readonly cell: Cell
+    readonly elementCount: number
+
+    // positions
+    readonly x: ArrayLike<number>
+    readonly y: ArrayLike<number>
+    readonly z: ArrayLike<number>
+}
+
+export interface DcdFile {
+    readonly header: DcdHeader
+    readonly frames: DcdFrame[]
+}
+
+export function _parseDcd(data: Uint8Array): DcdFile {
+    // http://www.ks.uiuc.edu/Research/vmd/plugins/molfile/dcdplugin.html
+
+    // The DCD format is structured as follows
+    //   (FORTRAN UNFORMATTED, with Fortran data type descriptions):
+    // HDR     NSET    ISTRT   NSAVC   5-ZEROS NATOM-NFREAT    DELTA   9-ZEROS
+    // `CORD'  #files  step 1  step    zeroes  (zero)          timestep  (zeroes)
+    //                         interval
+    // C*4     INT     INT     INT     5INT    INT             DOUBLE  9INT
+    // ==========================================================================
+    // NTITLE          TITLE
+    // INT (=2)        C*MAXTITL
+    //                 (=32)
+    // ==========================================================================
+    // NATOM
+    // #atoms
+    // INT
+    // ==========================================================================
+    // X(I), I=1,NATOM         (DOUBLE)
+    // Y(I), I=1,NATOM
+    // Z(I), I=1,NATOM
+    // ==========================================================================
+
+    const dv = new DataView(data.buffer)
+
+    const header: Mutable<DcdHeader> = Object.create(null)
+    const frames: DcdFrame[] = []
+
+    let nextPos = 0
+
+    // header block
+
+    const intView = new Int32Array(data.buffer, 0, 23)
+    const ef = intView[0] !== dv.getInt32(0) // endianess flag
+    // swap byte order when big endian (84 indicates little endian)
+    if (intView[0] !== 84) {
+        const n = data.byteLength
+        for (let i = 0; i < n; i += 4) {
+            dv.setFloat32(i, dv.getFloat32(i), true)
+        }
+    }
+    if (intView[0] !== 84) {
+        throw new Error('dcd bad format, header block start')
+    }
+
+    // format indicator, should read 'CORD'
+    const formatString = String.fromCharCode(
+        dv.getUint8(4), dv.getUint8(5),
+        dv.getUint8(6), dv.getUint8(7)
+    )
+    if (formatString !== 'CORD') {
+        throw new Error('dcd bad format, format string')
+    }
+    let isCharmm = false
+    let extraBlock = false
+    let fourDims = false
+    // version field in charmm, unused in X-PLOR
+    if (intView[22] !== 0) {
+        isCharmm = true
+        if (intView[12] !== 0) extraBlock = true
+        if (intView[13] === 1) fourDims = true
+    }
+    header.NSET = intView[2]
+    header.ISTART = intView[3]
+    header.NSAVC = intView[4]
+    header.NAMNF = intView[10]
+
+    if (isCharmm) {
+        header.DELTA = dv.getFloat32(44, ef)
+    } else {
+        header.DELTA = dv.getFloat64(44, ef)
+    }
+
+    if (intView[22] !== 84) {
+        throw new Error('dcd bad format, header block end')
+    }
+    nextPos = nextPos + 21 * 4 + 8
+
+    // title block
+
+    const titleEnd = dv.getInt32(nextPos, ef)
+    const titleStart = nextPos + 1
+    if ((titleEnd - 4) % 80 !== 0) {
+        throw new Error('dcd bad format, title block start')
+    }
+    header.TITLE = uint8ToString(data.subarray(titleStart, titleEnd))
+    if (dv.getInt32(titleStart + titleEnd + 4 - 1, ef) !== titleEnd) {
+        throw new Error('dcd bad format, title block end')
+    }
+
+    nextPos = nextPos + titleEnd + 8
+
+    // natom block
+
+    if (dv.getInt32(nextPos, ef) !== 4) {
+        throw new Error('dcd bad format, natom block start')
+    }
+    header.NATOM = dv.getInt32(nextPos + 4, ef)
+    if (dv.getInt32(nextPos + 8, ef) !== 4) {
+        throw new Error('dcd bad format, natom block end')
+    }
+    nextPos = nextPos + 4 + 8
+
+    // fixed atoms block
+
+    if (header.NAMNF > 0) {
+        // TODO read coordinates and indices of fixed atoms
+        throw new Error('dcd format with fixed atoms unsupported, aborting')
+    }
+
+    // frames
+
+    const natom = header.NATOM
+    const natom4 = natom * 4
+
+    for (let i = 0, n = header.NSET; i < n; ++i) {
+        const frame: Mutable<DcdFrame> = Object.create({
+            elementCount: natom
+        })
+
+        if (extraBlock) {
+            nextPos += 4 // block start
+            // cell: A, alpha, B, beta, gamma, C (doubles)
+            const size = Vec3.create(
+                dv.getFloat64(nextPos, ef),
+                dv.getFloat64(nextPos + 2 * 8, ef),
+                dv.getFloat64(nextPos + 5 * 8, ef)
+            )
+            const anglesInRadians = Vec3.create(
+                dv.getFloat64(nextPos + 1, ef),
+                dv.getFloat64(nextPos + 3 * 8, ef),
+                dv.getFloat64(nextPos + 4 * 8, ef)
+            )
+            frame.cell = Cell.create(size, anglesInRadians)
+            nextPos += 48
+            nextPos += 4 // block end
+        }
+
+        // xyz coordinates
+        for (let j = 0; j < 3; ++j) {
+            if (dv.getInt32(nextPos, ef) !== natom4) {
+                throw new Error(`dcd bad format, coord block start: ${i}, ${j}`)
+            }
+            nextPos += 4 // block start
+            const c = new Float32Array(data.buffer, nextPos, natom)
+            if (j === 0) frame.x = c
+            else if (j === 1) frame.y = c
+            else frame.z = c
+
+            nextPos += natom4
+            if (dv.getInt32(nextPos, ef) !== natom4) {
+                throw new Error(`dcd bad format, coord block end: ${i}, ${j}`)
+            }
+            nextPos += 4 // block end
+        }
+
+        if (fourDims) {
+            const bytes = dv.getInt32(nextPos, ef)
+            nextPos += 4 + bytes + 4 // block start + skip + block end
+        }
+
+        frames.push(frame)
+    }
+
+    return { header, frames }
+}
+
+export function parseDcd(data: Uint8Array) {
+    return Task.create<Result<DcdFile>>('Parse DCD', async ctx => {
+        try {
+            const dcdFile = _parseDcd(data)
+            return Result.success(dcdFile)
+        } catch (e) {
+            return Result.error(e)
+        }
+    })
+}

+ 12 - 15
src/mol-io/reader/ply/parser.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,6 +13,7 @@ import { TokenColumn } from '../common/text/column/token';
 
 // TODO add support for binary ply files
 // TODO parse elements asynchronously
+// TODO handle lists with appended properties
 
 interface State {
     data: string
@@ -79,7 +80,10 @@ function parseHeader(state: State) {
                     break
                 }
             }
-            if (isList && currentProperties.length !== 1) throw new Error('expected single list property')
+            if (isList && currentProperties.length !== 1) {
+                // TODO handle lists with appended properties
+                //      currently only the list part will be accessible
+            }
             if (isList) {
                 elementSpecs.push({
                     kind: 'list',
@@ -203,10 +207,8 @@ function parseListElement(state: State, spec: ListElementSpec) {
     let entryCount = 0
 
     for (let i = 0, il = count; i < il; ++i) {
-        // skip over row entry count as it is determined by line break
         Tokenizer.skipWhitespace(tokenizer)
-        Tokenizer.eatValue(tokenizer)
-
+        Tokenizer.markStart(tokenizer)
         while (Tokenizer.skipWhitespace(tokenizer) !== 10) {
             ++entryCount
             Tokenizer.markStart(tokenizer)
@@ -216,9 +218,6 @@ function parseListElement(state: State, spec: ListElementSpec) {
         offsets[i + 1] = entryCount
     }
 
-    // console.log(tokens.indices)
-    // console.log(offsets)
-
     /** holds row value entries transiently */
     const listValue = {
         entries: [] as number[],
@@ -233,12 +232,12 @@ function parseListElement(state: State, spec: ListElementSpec) {
         name: property.name,
         type: property.dataType,
         value: (row: number) => {
-            const start = offsets[row]
-            const end = offsets[row + 1]
-            for (let i = start; i < end; ++i) {
-                listValue.entries[i - start] = column.value(i)
+            const offset = offsets[row] + 1
+            const count = column.value(offset - 1)
+            for (let i = offset, il = offset + count; i < il; ++i) {
+                listValue.entries[i - offset] = column.value(i)
             }
-            listValue.count = end - start
+            listValue.count = count
             return listValue
         }
     })
@@ -248,8 +247,6 @@ async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<
     const state = State(data, ctx);
     ctx.update({ message: 'Parsing...', current: 0, max: data.length });
     parseHeader(state)
-    // console.log(state.comments)
-    // console.log(JSON.stringify(state.elementSpecs, undefined, 4))
     parseElements(state)
     const { elements, elementSpecs, comments } = state
     const elementNames = elementSpecs.map(s => s.name)

+ 213 - 0
src/mol-io/reader/psf/parser.ts

@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Task, RuntimeContext, chunkedSubtask } from '../../../mol-task'
+import { Tokenizer, TokenBuilder } from '../common/text/tokenizer'
+import { ReaderResult as Result } from '../result'
+import TokenColumn from '../common/text/column/token';
+import { Column } from '../../../mol-data/db';
+
+// http://www.ks.uiuc.edu/Training/Tutorials/namd/namd-tutorial-unix-html/node23.html
+
+export interface PsfFile {
+    readonly id: string
+    readonly title: string[]
+    readonly atoms: {
+        readonly count: number
+        readonly atomId: Column<number>
+        readonly segmentName: Column<string>
+        readonly residueId: Column<number>
+        readonly residueName: Column<string>
+        readonly atomName: Column<string>
+        readonly atomType: Column<string>
+        readonly charge: Column<number>
+        readonly mass: Column<number>
+    }
+    readonly bonds: {
+        readonly count: number
+        readonly atomIdA: Column<number>
+        readonly atomIdB: Column<number>
+    }
+}
+
+const { readLine, skipWhitespace, eatValue, eatLine, markStart } = Tokenizer;
+
+const reWhitespace = /\s+/
+const reTitle = /(^\*|REMARK)*/
+
+function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext) {
+    return {
+        tokenizer,
+        runtimeCtx,
+    }
+}
+type State = ReturnType<typeof State>
+
+async function handleAtoms(state: State, count: number): Promise<PsfFile['atoms']> {
+    const { tokenizer } = state
+
+    const atomId = TokenBuilder.create(tokenizer.data, count * 2)
+    const segmentName = TokenBuilder.create(tokenizer.data, count * 2)
+    const residueId = TokenBuilder.create(tokenizer.data, count * 2)
+    const residueName = TokenBuilder.create(tokenizer.data, count * 2)
+    const atomName = TokenBuilder.create(tokenizer.data, count * 2)
+    const atomType = TokenBuilder.create(tokenizer.data, count * 2)
+    const charge = TokenBuilder.create(tokenizer.data, count * 2)
+    const mass = TokenBuilder.create(tokenizer.data, count * 2)
+
+    const { length } = tokenizer
+    let linesAlreadyRead = 0
+    await chunkedSubtask(state.runtimeCtx, 10, void 0, chunkSize => {
+        const linesToRead = Math.min(count - linesAlreadyRead, chunkSize)
+        for (let i = 0; i < linesToRead; ++i) {
+            for (let j = 0; j < 8; ++j) {
+                skipWhitespace(tokenizer)
+                markStart(tokenizer)
+                eatValue(tokenizer)
+                switch (j) {
+                    case 0: TokenBuilder.addUnchecked(atomId, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 1: TokenBuilder.addUnchecked(segmentName, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 2: TokenBuilder.addUnchecked(residueId, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 3: TokenBuilder.addUnchecked(residueName, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 4: TokenBuilder.addUnchecked(atomName, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 5: TokenBuilder.addUnchecked(atomType, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 6: TokenBuilder.addUnchecked(charge, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 7: TokenBuilder.addUnchecked(mass, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                }
+            }
+            // ignore any extra columns
+            eatLine(tokenizer)
+            markStart(tokenizer)
+        }
+        linesAlreadyRead += linesToRead
+        return linesToRead
+    }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }))
+
+    return {
+        count,
+        atomId: TokenColumn(atomId)(Column.Schema.int),
+        segmentName: TokenColumn(segmentName)(Column.Schema.str),
+        residueId: TokenColumn(residueId)(Column.Schema.int),
+        residueName: TokenColumn(residueName)(Column.Schema.str),
+        atomName: TokenColumn(atomName)(Column.Schema.str),
+        atomType: TokenColumn(atomType)(Column.Schema.str),
+        charge: TokenColumn(charge)(Column.Schema.float),
+        mass: TokenColumn(mass)(Column.Schema.float)
+    }
+}
+
+async function handleBonds(state: State, count: number): Promise<PsfFile['bonds']> {
+    const { tokenizer } = state
+
+    const atomIdA = TokenBuilder.create(tokenizer.data, count * 2)
+    const atomIdB = TokenBuilder.create(tokenizer.data, count * 2)
+
+    const { length } = tokenizer
+    let bondsAlreadyRead = 0
+    await chunkedSubtask(state.runtimeCtx, 10, void 0, chunkSize => {
+        const bondsToRead = Math.min(count - bondsAlreadyRead, chunkSize)
+        for (let i = 0; i < bondsToRead; ++i) {
+            for (let j = 0; j < 2; ++j) {
+                skipWhitespace(tokenizer)
+                markStart(tokenizer)
+                eatValue(tokenizer)
+                switch (j) {
+                    case 0: TokenBuilder.addUnchecked(atomIdA, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                    case 1: TokenBuilder.addUnchecked(atomIdB, tokenizer.tokenStart, tokenizer.tokenEnd); break
+                }
+            }
+        }
+        bondsAlreadyRead += bondsToRead
+        return bondsToRead
+    }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length }))
+
+    return {
+        count,
+        atomIdA: TokenColumn(atomIdA)(Column.Schema.int),
+        atomIdB: TokenColumn(atomIdB)(Column.Schema.int),
+    }
+}
+
+function parseTitle(state: State, count: number) {
+    const title: string[] = []
+    for (let i = 0; i < count; ++i) {
+        const line = readLine(state.tokenizer)
+        title.push(line.replace(reTitle, '').trim())
+    }
+    return title
+}
+
+async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<PsfFile>> {
+    const tokenizer = Tokenizer(data);
+    const state = State(tokenizer, ctx);
+
+    let title = undefined as string[] | undefined
+    let atoms = undefined  as PsfFile['atoms'] | undefined
+    let bonds = undefined  as PsfFile['bonds'] | undefined
+
+    const id = readLine(state.tokenizer).trim()
+
+    while(tokenizer.tokenEnd < tokenizer.length) {
+        const line = readLine(state.tokenizer).trim()
+        if (line.includes('!NTITLE')) {
+            const numTitle = parseInt(line.split(reWhitespace)[0])
+            title = parseTitle(state, numTitle)
+        } else if (line.includes('!NATOM')) {
+            const numAtoms = parseInt(line.split(reWhitespace)[0])
+            atoms = await handleAtoms(state, numAtoms)
+        } else if (line.includes('!NBOND')) {
+            const numBonds = parseInt(line.split(reWhitespace)[0])
+            bonds = await handleBonds(state, numBonds)
+            break // TODO: don't break when the below are implemented
+        } else if (line.includes('!NTHETA')) {
+            // TODO
+        } else if (line.includes('!NPHI')) {
+            // TODO
+        } else if (line.includes('!NIMPHI')) {
+            // TODO
+        } else if (line.includes('!NDON')) {
+            // TODO
+        } else if (line.includes('!NACC')) {
+            // TODO
+        } else if (line.includes('!NNB')) {
+            // TODO
+        } else if (line.includes('!NGRP NST2')) {
+            // TODO
+        } else if (line.includes('!MOLNT')) {
+            // TODO
+        } else if (line.includes('!NUMLP NUMLPH')) {
+            // TODO
+        } else if (line.includes('!NCRTERM')) {
+            // TODO
+        }
+    }
+
+    if (title === undefined) {
+        title = []
+    }
+
+    if (atoms === undefined) {
+        return Result.error('no atoms data')
+    }
+
+    if (bonds === undefined) {
+        return Result.error('no bonds data')
+    }
+
+    const result: PsfFile = {
+        id,
+        title,
+        atoms,
+        bonds
+    }
+    return Result.success(result);
+}
+
+export function parsePsf(data: string) {
+    return Task.create<Result<PsfFile>>('Parse PSF', async ctx => {
+        return await parseInternal(data, ctx)
+    });
+}

+ 47 - 9
src/mol-math/geometry/centroid-helper.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 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 { Vec3 } from '../../mol-math/linear-algebra/3d';
@@ -12,7 +13,7 @@ export { CentroidHelper }
 class CentroidHelper {
     private count = 0;
 
-    center: Vec3 = Vec3.zero();
+    center: Vec3 = Vec3();
     radiusSq = 0;
 
     reset() {
@@ -46,21 +47,58 @@ class CentroidHelper {
 }
 
 namespace CentroidHelper {
-    const helper = new CentroidHelper(), p = Vec3.zero();
+    const helper = new CentroidHelper()
+    const posA = Vec3()
+    const posB = Vec3()
 
-    export function compute({ x, y, z }: { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }, to: Vec3) {
+    export function fromArrays({ x, y, z }: { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }, to: Sphere3D) {
         helper.reset();
         const n = x.length;
         for (let i = 0; i < n; i++) {
-            Vec3.set(p, x[i], y[i], z[i]);
-            helper.includeStep(p);
+            Vec3.set(posA, x[i], y[i], z[i]);
+            helper.includeStep(posA);
         }
         helper.finishedIncludeStep();
         for (let i = 0; i < n; i++) {
-            Vec3.set(p, x[i], y[i], z[i]);
-            helper.radiusStep(p);
+            Vec3.set(posA, x[i], y[i], z[i]);
+            helper.radiusStep(posA);
         }
-        Vec3.copy(to, helper.center);
+        Vec3.copy(to.center, helper.center);
+        to.radius = Math.sqrt(helper.radiusSq)
+        return to;
+    }
+
+    export function fromProvider(count: number, getter: (i: number, pos: Vec3) => void, to: Sphere3D) {
+        helper.reset();
+        for (let i = 0; i < count; i++) {
+            getter(i, posA)
+            helper.includeStep(posA);
+        }
+        helper.finishedIncludeStep();
+        for (let i = 0; i < count; i++) {
+            getter(i, posA)
+            helper.radiusStep(posA);
+        }
+        Vec3.copy(to.center, helper.center);
+        to.radius = Math.sqrt(helper.radiusSq)
+        return to;
+    }
+
+    export function fromPairProvider(count: number, getter: (i: number, posA: Vec3, posB: Vec3) => void, to: Sphere3D) {
+        helper.reset();
+        for (let i = 0; i < count; i++) {
+            getter(i, posA, posB)
+            helper.includeStep(posA);
+            helper.includeStep(posB);
+        }
+        helper.finishedIncludeStep();
+        for (let i = 0; i < count; i++) {
+            getter(i, posA, posB)
+            helper.radiusStep(posA);
+            helper.radiusStep(posB);
+        }
+        Vec3.copy(to.center, helper.center);
+        to.radius = Math.sqrt(helper.radiusSq)
         return to;
     }
 }

+ 14 - 17
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Michael Krone <michael.krone@uni-tuebingen.de>
@@ -13,7 +13,7 @@ import { Vec3, Tensor, Mat4, Vec2 } from '../../linear-algebra'
 import { ValueCell } from '../../../mol-util'
 import { createComputeRenderable, ComputeRenderable } from '../../../mol-gl/renderable'
 import { WebGLContext } from '../../../mol-gl/webgl/context';
-import { createTexture, Texture } from '../../../mol-gl/webgl/texture';
+import { Texture } from '../../../mol-gl/webgl/texture';
 import { decodeFloatRGB } from '../../../mol-util/float-packing';
 import { ShaderCode } from '../../../mol-gl/shader-code';
 import { createComputeRenderItem } from '../../../mol-gl/webgl/render-item';
@@ -50,9 +50,6 @@ export const GaussianDensityShaderCode = ShaderCode(
     { standardDerivatives: false, fragDepth: false }
 )
 
-/** name for shared framebuffer used for gpu gaussian surface operations */
-const FramebufferName = 'gaussian-density'
-
 export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): DensityData {
     // always use texture2d when the gaussian density needs to be downloaded from the GPU,
     // it's faster than texture3d
@@ -111,24 +108,24 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     const gridTexDim = Vec3.create(texDimX, texDimY, 0)
     const gridTexScale = Vec2.create(texDimX / powerOfTwoSize, texDimY / powerOfTwoSize)
 
-    const minDistanceTexture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest')
+    const minDistanceTexture = webgl.resources.texture('image-float32', 'rgba', 'float', 'nearest')
     minDistanceTexture.define(powerOfTwoSize, powerOfTwoSize)
 
     const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, gridTexDim, gridTexScale, smoothness, props.resolution)
 
     //
 
-    const { gl, framebufferCache, state } = webgl
+    const { gl, resources, state } = webgl
     const { uCurrentSlice, uCurrentX, uCurrentY } = renderable.values
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
     framebuffer.bind()
     setRenderingDefaults(webgl)
 
     if (!texture) {
-        texture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest')
+        texture = resources.texture('image-float32', 'rgba', 'float', 'nearest')
         texture.define(powerOfTwoSize, powerOfTwoSize)
-    } else if (texture.width !== powerOfTwoSize || texture.height !== powerOfTwoSize) {
+    } else if (texture.getWidth() !== powerOfTwoSize || texture.getHeight() !== powerOfTwoSize) {
         texture.define(powerOfTwoSize, powerOfTwoSize)
     }
 
@@ -174,11 +171,12 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
 }
 
 function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData {
+    const { gl, resources } = webgl
     const { smoothness } = props
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim } = prepareGaussianDensityData(position, box, radius, props)
     const [ dx, dy, dz ] = dim
-    const minDistanceTexture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest')
+    const minDistanceTexture = resources.texture('volume-float32', 'rgba', 'float', 'nearest')
     minDistanceTexture.define(dx, dy, dz)
 
     const gridTexScale = Vec2.create(1, 1)
@@ -187,15 +185,14 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
 
     //
 
-    const { gl, framebufferCache } = webgl
     const { uCurrentSlice } = renderable.values
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
     framebuffer.bind()
     setRenderingDefaults(webgl)
     gl.viewport(0, 0, dx, dy)
 
-    if (!texture) texture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest')
+    if (!texture) texture = resources.texture('volume-float32', 'rgba', 'float', 'nearest')
     texture.define(dx, dy, dz)
 
     function render(fbTex: Texture) {
@@ -279,7 +276,7 @@ function getGaussianDensityRenderable(webgl: WebGLContext, drawCount: number, po
         uResolution: ValueCell.create(resolution),
         tMinDistanceTex: ValueCell.create(minDistanceTexture),
 
-        dGridTexType: ValueCell.create(minDistanceTexture.depth > 0 ? '3d' : '2d'),
+        dGridTexType: ValueCell.create(minDistanceTexture.getDepth() > 0 ? '3d' : '2d'),
         dCalcType: ValueCell.create('minDistance'),
     }
 
@@ -356,7 +353,7 @@ function getTexture2dSize(gridDim: Vec3) {
 
 export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) {
     // console.time('fieldFromTexture2d')
-    const { framebufferCache } = ctx
+    const { resources } = ctx
     const [ dx, dy, dz ] = dim
     // const { width, height } = texture
     const [ width, height ] = texDim
@@ -371,7 +368,7 @@ export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec
     // const image = new Uint8Array(width * height * 4)
     const image = new Float32Array(width * height * 4)
 
-    const framebuffer = framebufferCache.get(FramebufferName).value
+    const framebuffer = resources.framebuffer()
     framebuffer.bind()
     texture.attachFramebuffer(framebuffer, 0)
     ctx.readPixels(0, 0, width, height, image)

+ 12 - 2
src/mol-math/geometry/lookup3d/common.ts

@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Box3D } from '../primitives/box3d'
@@ -23,9 +24,18 @@ export namespace Result {
         result.count = 0;
     }
 
-    export function create<T>(): Result<T> {
+    export function create<T = number>(): Result<T> {
         return { count: 0, indices: [], squaredDistances: [] };
     }
+
+    export function copy<T = number>(out: Result<T>, result: Result<T>) {
+        for (let i = 0; i < result.count; ++i) {
+            out.indices[i] = result.indices[i];
+            out.squaredDistances[i] = result.squaredDistances[i];
+        }
+        out.count = result.count
+        return out
+    }
 }
 
 export interface Lookup3D<T = number> {

+ 13 - 13
src/mol-math/geometry/lookup3d/grid.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 David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -17,19 +17,19 @@ interface GridLookup3D<T = number> extends Lookup3D<T> {
     readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
 }
 
-function GridLookup3D(data: PositionData, cellSizeOrCount?: Vec3 | number): GridLookup3D {
-    return new GridLookup3DImpl(data, cellSizeOrCount);
+function GridLookup3D<T extends number = number>(data: PositionData, cellSizeOrCount?: Vec3 | number): GridLookup3D<T> {
+    return new GridLookup3DImpl<T>(data, cellSizeOrCount);
 }
 
 export { GridLookup3D }
 
-class GridLookup3DImpl implements GridLookup3D<number> {
-    private ctx: QueryContext;
+class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
+    private ctx: QueryContext<T>;
     boundary: Lookup3D['boundary'];
     buckets: GridLookup3D['buckets'];
-    result: Result<number>
+    result: Result<T>
 
-    find(x: number, y: number, z: number, radius: number): Result<number> {
+    find(x: number, y: number, z: number, radius: number): Result<T> {
         this.ctx.x = x;
         this.ctx.y = y;
         this.ctx.z = z;
@@ -50,7 +50,7 @@ class GridLookup3DImpl implements GridLookup3D<number> {
 
     constructor(data: PositionData, cellSizeOrCount?: Vec3 | number) {
         const structure = build(data, cellSizeOrCount);
-        this.ctx = createContext(structure);
+        this.ctx = createContext<T>(structure);
         this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
         this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
         this.result = this.ctx.result
@@ -192,7 +192,7 @@ function build(data: PositionData, cellSizeOrCount?: Vec3 | number) {
     const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
     const { indices } = data;
 
-    const S = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min);
+    const S = Box3D.size(Vec3.zero(), expandedBox);
     let delta, size;
 
     const elementCount = OrderedSet.size(indices);
@@ -236,21 +236,21 @@ function build(data: PositionData, cellSizeOrCount?: Vec3 | number) {
     return _build(state);
 }
 
-interface QueryContext {
+interface QueryContext<T extends number = number> {
     grid: Grid3D,
     x: number,
     y: number,
     z: number,
     radius: number,
-    result: Result<number>,
+    result: Result<T>,
     isCheck: boolean
 }
 
-function createContext(grid: Grid3D): QueryContext {
+function createContext<T extends number = number>(grid: Grid3D): QueryContext<T> {
     return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, result: Result.create(), isCheck: false }
 }
 
-function query(ctx: QueryContext): boolean {
+function query<T extends number = number>(ctx: QueryContext<T>): boolean {
     const { min, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.grid;
     const { radius: inputRadius, isCheck, x, y, z, result } = ctx;
 

+ 4 - 2
src/mol-math/geometry/primitives/sphere3d.ts

@@ -107,13 +107,15 @@ namespace Sphere3D {
     }
 
     /** Expand sphere radius by another sphere */
-    export function expandBySphere(out: Sphere3D, sphere: Sphere3D) {
-        out.radius = Math.max(out.radius, Vec3.distance(out.center, sphere.center) + sphere.radius)
+    export function expandBySphere(out: Sphere3D, sphere: Sphere3D, by: Sphere3D) {
+        Vec3.copy(out.center, sphere.center)
+        out.radius = Math.max(sphere.radius, Vec3.distance(sphere.center, by.center) + by.radius)
         return out
     }
 
     /** Expand sphere radius by delta */
     export function expand(out: Sphere3D, sphere: Sphere3D, delta: number): Sphere3D {
+        Vec3.copy(out.center, sphere.center)
         out.radius = sphere.radius + delta
         return out
     }

+ 23 - 0
src/mol-math/geometry/spacegroup/cell.ts

@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from '../../linear-algebra'
+
+export { Cell }
+
+interface Cell {
+    readonly size: Vec3
+    readonly anglesInRadians: Vec3
+}
+
+function Cell() {
+    return Cell.empty()
+}
+
+namespace Cell {
+    export function create(size: Vec3, anglesInRadians: Vec3): Cell { return { size, anglesInRadians } }
+    export function empty(): Cell { return { size: Vec3(), anglesInRadians: Vec3() } }
+}

+ 43 - 42
src/mol-math/graph/int-adjacency-graph.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 David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -7,6 +7,7 @@
 
 import { arrayPickIndices, cantorPairing } from '../../mol-data/util';
 import { LinkedIndex, SortedArray } from '../../mol-data/int';
+import { AssignableArrayLike } from '../../mol-util/type-helpers';
 
 /**
  * Represent a graph using vertex adjacency list.
@@ -16,10 +17,10 @@ import { LinkedIndex, SortedArray } from '../../mol-data/int';
  *
  * Edge properties are indexed same as in the arrays a and b.
  */
-export interface IntAdjacencyGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase = {}> {
+export interface IntAdjacencyGraph<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase> {
     readonly offset: ArrayLike<number>,
-    readonly a: ArrayLike<number>,
-    readonly b: ArrayLike<number>,
+    readonly a: ArrayLike<VertexIndex>,
+    readonly b: ArrayLike<VertexIndex>,
     readonly vertexCount: number,
     readonly edgeCount: number,
     readonly edgeProps: Readonly<EdgeProps>
@@ -33,25 +34,25 @@ export interface IntAdjacencyGraph<EdgeProps extends IntAdjacencyGraph.EdgeProps
      *
      * `getEdgeIndex(i, j) === getEdgeIndex(j, i)`
      */
-    getEdgeIndex(i: number, j: number): number,
+    getEdgeIndex(i: VertexIndex, j: VertexIndex): number,
     /**
      * Get the edge index between i-th and j-th vertex.
      * -1 if the edge does not exist.
      *
      * `getEdgeIndex(i, j) !== getEdgeIndex(j, i)`
      */
-    getDirectedEdgeIndex(i: number, j: number): number,
-    getVertexEdgeCount(i: number): number
+    getDirectedEdgeIndex(i: VertexIndex, j: VertexIndex): number,
+    getVertexEdgeCount(i: VertexIndex): number
 }
 
 export namespace IntAdjacencyGraph {
     export type EdgePropsBase = { [name: string]: ArrayLike<any> }
 
-    class IntGraphImpl implements IntAdjacencyGraph<any> {
+    class IntGraphImpl<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase> implements IntAdjacencyGraph<VertexIndex, EdgeProps> {
         readonly vertexCount: number;
-        readonly edgeProps: object;
+        readonly edgeProps: EdgeProps;
 
-        getEdgeIndex(i: number, j: number): number {
+        getEdgeIndex(i: VertexIndex, j: VertexIndex): number {
             let a, b;
             if (i < j) { a = i; b = j; }
             else { a = j; b = i; }
@@ -61,28 +62,28 @@ export namespace IntAdjacencyGraph {
             return -1;
         }
 
-        getDirectedEdgeIndex(i: number, j: number): number {
+        getDirectedEdgeIndex(i: VertexIndex, j: VertexIndex): number {
             for (let t = this.offset[i], _t = this.offset[i + 1]; t < _t; t++) {
                 if (this.b[t] === j) return t;
             }
             return -1;
         }
 
-        getVertexEdgeCount(i: number): number {
+        getVertexEdgeCount(i: VertexIndex): number {
             return this.offset[i + 1] - this.offset[i];
         }
 
-        constructor(public offset: ArrayLike<number>, public a: ArrayLike<number>, public b: ArrayLike<number>, public edgeCount: number, edgeProps?: any) {
+        constructor(public offset: ArrayLike<number>, public a: ArrayLike<VertexIndex>, public b: ArrayLike<VertexIndex>, public edgeCount: number, edgeProps?: EdgeProps) {
             this.vertexCount = offset.length - 1;
-            this.edgeProps = edgeProps || {};
+            this.edgeProps = (edgeProps || {}) as EdgeProps;
         }
     }
 
-    export function create<EdgeProps extends IntAdjacencyGraph.EdgePropsBase = {}>(offset: ArrayLike<number>, a: ArrayLike<number>, b: ArrayLike<number>, edgeCount: number, edgeProps?: EdgeProps): IntAdjacencyGraph<EdgeProps> {
-        return new IntGraphImpl(offset, a, b, edgeCount, edgeProps) as IntAdjacencyGraph<EdgeProps>;
+    export function create<VertexIndex extends number, EdgeProps extends IntAdjacencyGraph.EdgePropsBase>(offset: ArrayLike<number>, a: ArrayLike<VertexIndex>, b: ArrayLike<VertexIndex>, edgeCount: number, edgeProps?: EdgeProps): IntAdjacencyGraph<VertexIndex, EdgeProps> {
+        return new IntGraphImpl(offset, a, b, edgeCount, edgeProps) as IntAdjacencyGraph<VertexIndex, EdgeProps>;
     }
 
-    export class EdgeBuilder {
+    export class EdgeBuilder<VertexIndex extends number> {
         private bucketFill: Int32Array;
         private current = 0;
         private curA: number = 0;
@@ -92,11 +93,11 @@ export namespace IntAdjacencyGraph {
         edgeCount: number;
         /** the size of the A and B arrays */
         slotCount: number;
-        a: Int32Array;
-        b: Int32Array;
+        a: AssignableArrayLike<VertexIndex>;
+        b: AssignableArrayLike<VertexIndex>;
 
-        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase = {}>(edgeProps?: EdgeProps) {
-            return create(this.offsets, this.a, this.b, this.edgeCount, edgeProps);
+        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase>(edgeProps: EdgeProps) {
+            return create<VertexIndex, EdgeProps>(this.offsets, this.a, this.b, this.edgeCount, edgeProps);
         }
 
         /**
@@ -139,7 +140,7 @@ export namespace IntAdjacencyGraph {
             prop[this.curB] = value;
         }
 
-        constructor(public vertexCount: number, public xs: ArrayLike<number>, public ys: ArrayLike<number>) {
+        constructor(public vertexCount: number, public xs: ArrayLike<VertexIndex>, public ys: ArrayLike<VertexIndex>) {
             this.edgeCount = xs.length;
             this.offsets = new Int32Array(this.vertexCount + 1);
             this.bucketFill = new Int32Array(this.vertexCount);
@@ -155,12 +156,12 @@ export namespace IntAdjacencyGraph {
             }
             this.offsets[this.vertexCount] = offset;
             this.slotCount = offset;
-            this.a = new Int32Array(offset);
-            this.b = new Int32Array(offset);
+            this.a = new Int32Array(offset) as unknown as AssignableArrayLike<VertexIndex>;
+            this.b = new Int32Array(offset) as unknown as AssignableArrayLike<VertexIndex>;
         }
     }
 
-    export class DirectedEdgeBuilder {
+    export class DirectedEdgeBuilder<VertexIndex extends number> {
         private bucketFill: Int32Array;
         private current = 0;
         private curA: number = 0;
@@ -172,7 +173,7 @@ export namespace IntAdjacencyGraph {
         a: Int32Array;
         b: Int32Array;
 
-        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase = {}>(edgeProps?: EdgeProps) {
+        createGraph<EdgeProps extends IntAdjacencyGraph.EdgePropsBase>(edgeProps: EdgeProps) {
             return create(this.offsets, this.a, this.b, this.edgeCount, edgeProps);
         }
 
@@ -209,7 +210,7 @@ export namespace IntAdjacencyGraph {
             prop[this.curA] = value;
         }
 
-        constructor(public vertexCount: number, public xs: ArrayLike<number>, public ys: ArrayLike<number>) {
+        constructor(public vertexCount: number, public xs: ArrayLike<VertexIndex>, public ys: ArrayLike<VertexIndex>) {
             this.edgeCount = xs.length;
             this.offsets = new Int32Array(this.vertexCount + 1);
             this.bucketFill = new Int32Array(this.vertexCount);
@@ -229,12 +230,12 @@ export namespace IntAdjacencyGraph {
         }
     }
 
-    export class UniqueEdgeBuilder {
-        private xs: number[] = [];
-        private ys: number[] = [];
+    export class UniqueEdgeBuilder<VertexIndex extends number> {
+        private xs: VertexIndex[] = [];
+        private ys: VertexIndex[] = [];
         private included = new Set<number>();
 
-        addEdge(i: number, j: number) {
+        addEdge(i: VertexIndex, j: VertexIndex) {
             let u = i, v = j;
             if (i > j) { u = j; v = i; }
             const id = cantorPairing(u, v);
@@ -245,7 +246,7 @@ export namespace IntAdjacencyGraph {
             return true;
         }
 
-        getGraph(): IntAdjacencyGraph {
+        getGraph(): IntAdjacencyGraph<VertexIndex, {}> {
             return fromVertexPairs(this.vertexCount, this.xs, this.ys);
         }
 
@@ -258,13 +259,13 @@ export namespace IntAdjacencyGraph {
         }
     }
 
-    export function fromVertexPairs(vertexCount: number, xs: number[], ys: number[]) {
+    export function fromVertexPairs<V extends number>(vertexCount: number, xs: V[], ys: V[]) {
         const graphBuilder = new IntAdjacencyGraph.EdgeBuilder(vertexCount, xs, ys);
         graphBuilder.addAllEdges();
-        return graphBuilder.createGraph();
+        return graphBuilder.createGraph({});
     }
 
-    export function induceByVertices<P extends IntAdjacencyGraph.EdgePropsBase>(graph: IntAdjacencyGraph<P>, vertexIndices: ArrayLike<number>): IntAdjacencyGraph<P> {
+    export function induceByVertices<V extends number, P extends IntAdjacencyGraph.EdgePropsBase>(graph: IntAdjacencyGraph<V, P>, vertexIndices: ArrayLike<number>): IntAdjacencyGraph<V, P> {
         const { b, offset, vertexCount, edgeProps } = graph;
         const vertexMap = new Int32Array(vertexCount);
         for (let i = 0, _i = vertexIndices.length; i < _i; i++) vertexMap[vertexIndices[i]] = i + 1;
@@ -279,8 +280,8 @@ export namespace IntAdjacencyGraph {
 
         const newOffsets = new Int32Array(vertexIndices.length + 1);
         const edgeIndices = new Int32Array(2 * newEdgeCount);
-        const newA = new Int32Array(2 * newEdgeCount);
-        const newB = new Int32Array(2 * newEdgeCount);
+        const newA = new Int32Array(2 * newEdgeCount) as unknown as AssignableArrayLike<V>;
+        const newB = new Int32Array(2 * newEdgeCount) as unknown as AssignableArrayLike<V>;
         let eo = 0, vo = 0;
         for (let i = 0; i < vertexCount; i++) {
             if (vertexMap[i] === 0) continue;
@@ -289,8 +290,8 @@ export namespace IntAdjacencyGraph {
                 const bb = vertexMap[b[j]];
                 if (bb === 0) continue;
 
-                newA[eo] = aa;
-                newB[eo] = bb - 1;
+                newA[eo] = aa as V;
+                newB[eo] = bb - 1 as V;
                 edgeIndices[eo] = j;
                 eo++;
             }
@@ -305,7 +306,7 @@ export namespace IntAdjacencyGraph {
         return create(newOffsets, newA, newB, newEdgeCount, newEdgeProps);
     }
 
-    export function connectedComponents(graph: IntAdjacencyGraph): { componentCount: number, componentIndex: Int32Array } {
+    export function connectedComponents(graph: IntAdjacencyGraph<any, any>): { componentCount: number, componentIndex: Int32Array } {
         const vCount = graph.vertexCount;
 
         if (vCount === 0) return { componentCount: 0, componentIndex: new Int32Array(0) };
@@ -357,7 +358,7 @@ export namespace IntAdjacencyGraph {
      *
      * Returns true if verticesA and verticesB are intersecting.
      */
-    export function areVertexSetsConnected(graph: IntAdjacencyGraph, verticesA: SortedArray<number>, verticesB: SortedArray<number>, maxDistance: number): boolean {
+    export function areVertexSetsConnected(graph: IntAdjacencyGraph<any, any>, verticesA: SortedArray<number>, verticesB: SortedArray<number>, maxDistance: number): boolean {
         // check if A and B are intersecting, this handles maxDistance = 0
         if (SortedArray.areIntersecting(verticesA, verticesB)) return true;
         if (maxDistance < 1) return false;
@@ -371,7 +372,7 @@ export namespace IntAdjacencyGraph {
     }
 }
 
-function areVertexSetsConnectedImpl(graph: IntAdjacencyGraph, frontier: ArrayLike<number>, target: SortedArray<number>, distance: number, visited: Set<number>): boolean {
+function areVertexSetsConnectedImpl(graph: IntAdjacencyGraph<any, any>, frontier: ArrayLike<number>, target: SortedArray<number>, distance: number, visited: Set<number>): boolean {
     const { b: neighbor, offset } = graph;
     const newFrontier: number[] = [];
 

+ 58 - 8
src/mol-math/graph/inter-unit-graph.ts

@@ -1,10 +1,12 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 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 { UniqueArray } from '../../mol-data/generic'
+
 export { InterUnitGraph }
 
 class InterUnitGraph<Unit extends InterUnitGraph.UnitBase, VertexIndex extends number, EdgeProps extends InterUnitGraph.EdgePropsBase = {}> {
@@ -44,11 +46,11 @@ class InterUnitGraph<Unit extends InterUnitGraph.UnitBase, VertexIndex extends n
         return this.vertexKeyIndex.get(InterUnitGraph.getVertexKey(index, unit)) || []
     }
 
-    constructor(private map: Map<number, InterUnitGraph.UnitPairEdges<Unit, VertexIndex, EdgeProps>[]>) {
+    constructor(protected readonly map: Map<number, InterUnitGraph.UnitPairEdges<Unit, VertexIndex, EdgeProps>[]>) {
         let count = 0
         const edges: (InterUnitGraph.Edge<Unit, VertexIndex, EdgeProps>)[] = []
         const edgeKeyIndex = new Map<string, number>()
-        const elementKeyIndex = new Map<string, number[]>()
+        const vertexKeyIndex = new Map<string, number[]>()
 
         this.map.forEach(pairEdgesArray => {
             pairEdgesArray.forEach(pairEdges => {
@@ -57,12 +59,12 @@ class InterUnitGraph<Unit extends InterUnitGraph.UnitBase, VertexIndex extends n
                     pairEdges.getEdges(indexA).forEach(edgeInfo => {
                         const { unitA, unitB } = pairEdges
 
-                        const edgeKey = InterUnitGraph.getEdgeKey<Unit, VertexIndex>(indexA, unitA, edgeInfo.indexB, unitB)
+                        const edgeKey = InterUnitGraph.getEdgeKey(indexA, unitA, edgeInfo.indexB, unitB)
                         edgeKeyIndex.set(edgeKey, edges.length)
 
-                        const elementKey = InterUnitGraph.getVertexKey(indexA, unitA)
-                        const e = elementKeyIndex.get(elementKey)
-                        if (e === undefined) elementKeyIndex.set(elementKey, [edges.length])
+                        const vertexKey = InterUnitGraph.getVertexKey(indexA, unitA)
+                        const e = vertexKeyIndex.get(vertexKey)
+                        if (e === undefined) vertexKeyIndex.set(vertexKey, [edges.length])
                         else e.push(edges.length)
 
                         edges.push({ ...edgeInfo, indexA, unitA, unitB })
@@ -74,7 +76,7 @@ class InterUnitGraph<Unit extends InterUnitGraph.UnitBase, VertexIndex extends n
         this.edgeCount = count
         this.edges = edges
         this.edgeKeyIndex = edgeKeyIndex
-        this.vertexKeyIndex = elementKeyIndex
+        this.vertexKeyIndex = vertexKeyIndex
     }
 }
 
@@ -123,6 +125,54 @@ namespace InterUnitGraph {
     export function getVertexKey<Unit extends UnitBase, VertexIndex extends number>(index: VertexIndex, unit: Unit) {
         return `${index}|${unit.id}`
     }
+
+    //
+
+    function addMapEntry<A, B>(map: Map<A, B[]>, a: A, b: B) {
+        if (map.has(a)) map.get(a)!.push(b);
+        else map.set(a, [b]);
+    }
+
+
+    export class Builder<Unit extends InterUnitGraph.UnitBase, VertexIndex extends number, EdgeProps extends InterUnitGraph.EdgePropsBase = {}> {
+        private uA: Unit
+        private uB: Unit
+        private mapAB: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>
+        private mapBA: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>
+        private linkedA: UniqueArray<VertexIndex, VertexIndex>
+        private linkedB: UniqueArray<VertexIndex, VertexIndex>
+        private linkCount: number
+
+        private map = new Map<number, UnitPairEdges<Unit, VertexIndex, EdgeProps>[]>();
+
+        startUnitPair(unitA: Unit, unitB: Unit) {
+            this.uA = unitA
+            this.uB = unitB
+            this.mapAB = new Map()
+            this.mapBA = new Map()
+            this.linkedA = UniqueArray.create()
+            this.linkedB = UniqueArray.create()
+            this.linkCount = 0
+        }
+
+        finishUnitPair() {
+            if (this.linkCount === 0) return
+            addMapEntry(this.map, this.uA.id, new UnitPairEdges(this.uA, this.uB, this.linkCount, this.linkedA.array, this.mapAB))
+            addMapEntry(this.map, this.uB.id, new UnitPairEdges(this.uB, this.uA, this.linkCount, this.linkedB.array, this.mapBA))
+        }
+
+        add(indexA: VertexIndex, indexB: VertexIndex, props: EdgeProps) {
+            addMapEntry(this.mapAB, indexA, { indexB, props })
+            addMapEntry(this.mapBA, indexB, { indexB: indexA, props })
+            UniqueArray.add(this.linkedA, indexA, indexA)
+            UniqueArray.add(this.linkedB, indexB, indexB)
+            this.linkCount += 1
+        }
+
+        getMap(): Map<number, InterUnitGraph.UnitPairEdges<Unit, VertexIndex, EdgeProps>[]> {
+            return this.map;
+        }
+    }
 }
 
 const emptyArray: any[] = [];

+ 3 - 3
src/mol-math/linear-algebra/3d.ts

@@ -29,6 +29,6 @@ export { Mat4, Mat3, Vec2, Vec3, Vec4, Quat, EPSILON }
 
 export type Vec<T> =
     T extends 4 ? Vec4 :
-    T extends 3 ? Vec3 :
-    T extends 2 ? Vec2 :
-    number[]
+        T extends 3 ? Vec3 :
+            T extends 2 ? Vec2 :
+                number[]

+ 34 - 1
src/mol-math/linear-algebra/3d/mat4.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 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>
@@ -739,6 +739,37 @@ namespace Mat4 {
         return true;
     }
 
+    /**
+     * Check if the matrix has only translation and uniform scaling
+     * [ S  0  0  X ]
+     * [ 0  S  0  Y ]
+     * [ 0  0  S  Z ]
+     * [ 0  0  0  1 ]
+     */
+    export function isTranslationAndUniformScaling(a: Mat4, eps?: number) {
+        return _isTranslationAndUniformScaling(a, typeof eps !== 'undefined' ? eps : EPSILON)
+    }
+
+    function _isTranslationAndUniformScaling(a: Mat4, eps: number) {
+        const a00 = a[0]
+        return (
+            // 0 base scaling
+            equalEps(a[1], 0, eps) &&
+            equalEps(a[2], 0, eps) &&
+            equalEps(a[3], 0, eps) &&
+            equalEps(a[4], 0, eps) &&
+            equalEps(a[5], a00, eps) &&
+            equalEps(a[6], 0, eps) &&
+            equalEps(a[7], 0, eps) &&
+            equalEps(a[8], 0, eps) &&
+            equalEps(a[9], 0, eps) &&
+            equalEps(a[10], a00, eps) &&
+            equalEps(a[11], 0, eps) &&
+            // 12, 13, 14 translation can be anything
+            equalEps(a[15], 1, eps)
+        )
+    }
+
     export function fromQuat(out: Mat4, q: Quat) {
         const x = q[0], y = q[1], z = q[2], w = q[3];
         const x2 = x + x;
@@ -1007,6 +1038,8 @@ namespace Mat4 {
     export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), yAxis)
     /** Rotation matrix for 180deg around y-axis */
     export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), yAxis)
+    /** Rotation matrix for 270deg around y-axis */
+    export const rotY270: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(270), yAxis)
     /** Rotation matrix for 90deg around z-axis */
     export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), zAxis)
     /** Rotation matrix for 180deg around z-axis */

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