Selaa lähdekoodia

Merge branch 'master' of https://github.com/molstar/molstar into pr/yakomaxa/514

Alexander Rose 2 vuotta sitten
vanhempi
commit
d08776bf19
97 muutettua tiedostoa jossa 2260 lisäystä ja 986 poistoa
  1. 47 4
      CHANGELOG.md
  2. 2 2
      README.md
  3. 302 232
      package-lock.json
  4. 26 23
      package.json
  5. 1 1
      src/apps/docking-viewer/index.ts
  6. 3 3
      src/apps/docking-viewer/viewport.tsx
  7. 4 1
      src/apps/viewer/app.ts
  8. 9 0
      src/apps/viewer/embedded.html
  9. BIN
      src/extensions/backgrounds/images/cells.jpg
  10. 90 0
      src/extensions/backgrounds/index.ts
  11. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg
  12. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg
  13. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg
  14. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg
  15. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg
  16. BIN
      src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg
  17. 10 0
      src/extensions/backgrounds/typings.d.ts
  18. 25 17
      src/extensions/dnatco/confal-pyramids/property.ts
  19. 74 54
      src/extensions/dnatco/confal-pyramids/representation.ts
  20. 7 6
      src/extensions/dnatco/confal-pyramids/types.ts
  21. 87 247
      src/extensions/dnatco/confal-pyramids/util.ts
  22. 1 0
      src/extensions/mp4-export/encoder.ts
  23. 1 4
      src/extensions/rcsb/graphql/types.ts
  24. 43 10
      src/mol-canvas3d/canvas3d.ts
  25. 461 0
      src/mol-canvas3d/passes/background.ts
  26. 49 45
      src/mol-canvas3d/passes/draw.ts
  27. 2 2
      src/mol-canvas3d/passes/fxaa.ts
  28. 12 3
      src/mol-canvas3d/passes/image.ts
  29. 4 4
      src/mol-canvas3d/passes/marking.ts
  30. 10 10
      src/mol-canvas3d/passes/multi-sample.ts
  31. 3 2
      src/mol-canvas3d/passes/passes.ts
  32. 54 24
      src/mol-canvas3d/passes/postprocessing.ts
  33. 2 2
      src/mol-canvas3d/passes/smaa.ts
  34. 11 1
      src/mol-canvas3d/passes/wboit.ts
  35. 6 6
      src/mol-geo/geometry/texture-mesh/color-smoothing.ts
  36. 2 2
      src/mol-gl/compute/grid3d.ts
  37. 7 7
      src/mol-gl/compute/histogram-pyramid/reduction.ts
  38. 2 2
      src/mol-gl/compute/histogram-pyramid/sum.ts
  39. 4 4
      src/mol-gl/compute/marching-cubes/active-voxels.ts
  40. 2 2
      src/mol-gl/compute/marching-cubes/isosurface.ts
  41. 2 2
      src/mol-gl/compute/util.ts
  42. 13 9
      src/mol-gl/renderer.ts
  43. 30 2
      src/mol-gl/scene.ts
  44. 2 0
      src/mol-gl/shader-code.ts
  45. 85 0
      src/mol-gl/shader/background.frag.ts
  46. 12 0
      src/mol-gl/shader/background.vert.ts
  47. 0 1
      src/mol-gl/shader/chunks/color-frag-params.glsl.ts
  48. 0 1
      src/mol-gl/shader/chunks/color-vert-params.glsl.ts
  49. 0 2
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  50. 3 3
      src/mol-gl/shader/chunks/common-vert-params.glsl.ts
  51. 5 5
      src/mol-gl/shader/cylinders.frag.ts
  52. 7 7
      src/mol-gl/shader/spheres.frag.ts
  53. 6 5
      src/mol-gl/webgl/context.ts
  54. 3 3
      src/mol-gl/webgl/render-item.ts
  55. 10 2
      src/mol-gl/webgl/resources.ts
  56. 29 0
      src/mol-gl/webgl/state.ts
  57. 119 1
      src/mol-gl/webgl/texture.ts
  58. 1 1
      src/mol-io/reader/cif/schema/bird.ts
  59. 1 1
      src/mol-io/reader/cif/schema/ccd.ts
  60. 1 1
      src/mol-io/reader/cif/schema/mmcif.ts
  61. 6 6
      src/mol-math/geometry/gaussian-density/gpu.ts
  62. 11 3
      src/mol-math/geometry/primitives/box3d.ts
  63. 1 1
      src/mol-math/linear-algebra/3d/vec3.ts
  64. 4 1
      src/mol-model-formats/structure/mol.ts
  65. 4 1
      src/mol-model-formats/structure/mol2.ts
  66. 1 1
      src/mol-model-props/common/custom-element-property.ts
  67. 48 14
      src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
  68. 31 10
      src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
  69. 16 0
      src/mol-model-props/computed/representations/shared.ts
  70. 0 10
      src/mol-model/structure/structure/carbohydrates/constants.ts
  71. 32 4
      src/mol-model/structure/structure/structure.ts
  72. 14 6
      src/mol-model/structure/structure/unit/bonds/inter-compute.ts
  73. 4 1
      src/mol-model/structure/structure/unit/bonds/intra-compute.ts
  74. 21 5
      src/mol-model/structure/structure/util/superposition-sifts-mapping.ts
  75. 13 7
      src/mol-plugin-state/actions/file.ts
  76. 2 1
      src/mol-plugin-state/actions/structure.ts
  77. 25 21
      src/mol-plugin-state/builder/structure/representation-preset.ts
  78. 28 7
      src/mol-plugin-ui/custom/volume.tsx
  79. 21 9
      src/mol-plugin-ui/viewport/help.tsx
  80. 14 2
      src/mol-plugin-ui/viewport/simple-settings.tsx
  81. 14 3
      src/mol-plugin/behavior/behavior.ts
  82. 163 84
      src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
  83. 3 1
      src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
  84. 6 2
      src/mol-plugin/config.ts
  85. 1 1
      src/mol-plugin/context.ts
  86. 3 8
      src/mol-plugin/features.ts
  87. 3 1
      src/mol-plugin/util/viewport-screenshot.ts
  88. 4 0
      src/mol-util/binding.ts
  89. 41 0
      src/mol-util/single-async-queue.ts
  90. 4 1
      src/tests/browser/marching-cubes.ts
  91. 4 1
      src/tests/browser/render-lines.ts
  92. 4 1
      src/tests/browser/render-mesh.ts
  93. 4 1
      src/tests/browser/render-shape.ts
  94. 4 1
      src/tests/browser/render-spheres.ts
  95. 4 2
      src/tests/browser/render-structure.ts
  96. 4 1
      src/tests/browser/render-text.ts
  97. 6 2
      webpack.config.common.js

+ 47 - 4
CHANGELOG.md

@@ -6,12 +6,55 @@ Note that since we don't clearly distinguish between a public and private interf
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
-- Fix defaultAttribs handling in Canvas3DContext.fromCanvas
-- Add custom labels to Confal pyramids
-- Improve naming of some internal types in Confal pyramids extension coordinate
-- Add example mmCIF file with categories necessary to display Confal pyramids
+- [Fix] Clone ``Canvas3DParams`` when creating a ``Canvas3D`` instance to prevent shared state between multiple instances
+- Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
+- Add ``parentDisplay`` param for interactions representation.
 - [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
 - [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
 
 
+## [v3.16.0] - 2022-08-25
+
+- Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
+- Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
+- Fix ``ViewportHelpContent`` Mouse Controls section
+
+## [v3.15.0] - 2022-08-23
+
+- Fix wboit in Safari >=15 (add missing depth renderbuffer to wboit pass)
+- Add 'Around Camera' option to Volume streaming
+- Avoid queuing more than one update in Volume streaming
+
+## [v3.14.0] - 2022-08-20
+
+- Expose inter-bonds compute params in structure
+- Improve performance of inter/intra-bonds compute
+- Fix defaultAttribs handling in Canvas3DContext.fromCanvas
+- Confal pyramids extension improvements
+    - Add custom labels to Confal pyramids
+    - Improve naming of some internal types in Confal pyramids extension coordinate
+    - Add example mmCIF file with categories necessary to display Confal pyramids
+    - Change the lookup logic of NtC steps from residues
+- Add support for download of gzipped files
+- Don't filter IndexPairBonds by element-based rules in MOL/SDF and MOL2 (without symmetry) models
+- Fix Glycam Saccharide Names used by default
+- Fix GPU surfaces rendering in Safari with WebGL2
+- Add ``fov`` (Field of View) Canvas3D parameter
+- Add ``sceneRadiusFactor`` Canvas3D parameter
+- Add background pass (skybox, image, horizontal/radial gradient)
+    - Set simple-settings presets via ``PluginConfig.Background.Styles``
+    - Example presets in new backgrounds extension
+    - Load skybox/image from URL or File (saved in session)
+    - Opacity, saturation, lightness controls for skybox/image
+    - Coverage (viewport or canvas) controls for image/gradient
+- [Breaking] ``AssetManager`` needs to be passed to various graphics related classes
+- Fix SSAO renderable initialization
+- Reduce number of webgl state changes
+    - Add ``viewport`` and ``scissor`` to state object
+    - Add ``hasOpaque`` to scene object
+- Handle edge cases where some renderables would not get (correctly) rendered
+    - Fix text background rendering for opaque text
+    - Fix helper scenes not shown when rendering directly to draw target
+- Fix ``CustomElementProperty`` coloring not working
+
 ## [v3.13.0] - 2022-07-24
 ## [v3.13.0] - 2022-07-24
 
 
 - Fix: only update camera state if manualReset is off (#494)
 - Fix: only update camera state if manualReset is off (#494)

+ 2 - 2
README.md

@@ -126,7 +126,7 @@ and navigate to `build/viewer`
 
 
 **GraphQL schemas**
 **GraphQL schemas**
 
 
-    node node_modules//@graphql-codegen/cli/bin -c src/extensions/rcsb/graphql/codegen.yml
+    node node_modules/@graphql-codegen/cli/cjs/bin -c src/extensions/rcsb/graphql/codegen.yml
 
 
 ### Other scripts
 ### Other scripts
 **Create chem comp bond table**
 **Create chem comp bond table**
@@ -152,7 +152,7 @@ Or
     node lib/commonjs/cli/cif2bcif
     node lib/commonjs/cli/cif2bcif
 
 
 E.g.
 E.g.
- 
+
     node lib/commonjs/cli/cif2bcif src.cif out.bcif.gz
     node lib/commonjs/cli/cif2bcif src.cif out.bcif.gz
     node lib/commonjs/cli/cif2bcif src.bcif.gz out.cif
     node lib/commonjs/cli/cif2bcif src.bcif.gz out.cif
 
 

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 302 - 232
package-lock.json


+ 26 - 23
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "molstar",
   "name": "molstar",
-  "version": "3.13.0",
+  "version": "3.16.0",
   "description": "A comprehensive macromolecular library.",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
   "repository": {
@@ -20,7 +20,7 @@
     "rebuild": "npm run clean && npm run build",
     "rebuild": "npm run clean && npm run build",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
-    "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
+    "build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/",
     "build-webpack": "webpack --mode production --config ./webpack.config.production.js",
     "build-webpack": "webpack --mode production --config ./webpack.config.production.js",
     "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
     "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
     "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
     "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
@@ -28,7 +28,7 @@
     "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
     "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
     "watch-tsc": "tsc --watch --incremental",
     "watch-tsc": "tsc --watch --incremental",
     "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
     "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
-    "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
+    "watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch",
     "watch-webpack": "webpack -w --mode development --stats minimal",
     "watch-webpack": "webpack -w --mode development --stats minimal",
     "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
     "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
     "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
     "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
@@ -75,7 +75,9 @@
       "node_modules",
       "node_modules",
       "lib"
       "lib"
     ],
     ],
-    "testURL": "http://localhost/",
+    "testEnvironmentOptions": {
+      "url": "http://localhost/"
+    },
     "testRegex": "\\.spec\\.ts$"
     "testRegex": "\\.spec\\.ts$"
   },
   },
   "author": "Mol* Contributors",
   "author": "Mol* Contributors",
@@ -88,34 +90,35 @@
     "Michal Malý <michal.maly@ibt.cas.cz>",
     "Michal Malý <michal.maly@ibt.cas.cz>",
     "Jiří Černý <jiri.cerny@ibt.cas.cz>",
     "Jiří Černý <jiri.cerny@ibt.cas.cz>",
     "Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
     "Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
+    "Adam Midlik <midlik@gmail.com>",
     "Koya Sakuma <koya.sakuma.work@gmail.com>"
     "Koya Sakuma <koya.sakuma.work@gmail.com>"
   ],
   ],
   "license": "MIT",
   "license": "MIT",
   "devDependencies": {
   "devDependencies": {
-    "@graphql-codegen/add": "^3.2.0",
-    "@graphql-codegen/cli": "^2.9.1",
-    "@graphql-codegen/time": "^3.2.0",
-    "@graphql-codegen/typescript": "^2.7.2",
-    "@graphql-codegen/typescript-graphql-files-modules": "^2.2.0",
-    "@graphql-codegen/typescript-graphql-request": "^4.5.2",
-    "@graphql-codegen/typescript-operations": "^2.5.2",
+    "@graphql-codegen/add": "^3.2.1",
+    "@graphql-codegen/cli": "^2.11.6",
+    "@graphql-codegen/time": "^3.2.1",
+    "@graphql-codegen/typescript": "^2.7.3",
+    "@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
+    "@graphql-codegen/typescript-graphql-request": "^4.5.3",
+    "@graphql-codegen/typescript-operations": "^2.5.3",
     "@types/cors": "^2.8.12",
     "@types/cors": "^2.8.12",
     "@types/gl": "^4.1.1",
     "@types/gl": "^4.1.1",
-    "@types/jest": "^28.1.6",
-    "@types/react": "^18.0.15",
+    "@types/jest": "^28.1.7",
+    "@types/react": "^18.0.17",
     "@types/react-dom": "^18.0.6",
     "@types/react-dom": "^18.0.6",
-    "@typescript-eslint/eslint-plugin": "^5.30.7",
-    "@typescript-eslint/parser": "^5.30.7",
+    "@typescript-eslint/eslint-plugin": "^5.33.1",
+    "@typescript-eslint/parser": "^5.33.1",
     "benchmark": "^2.1.4",
     "benchmark": "^2.1.4",
     "concurrently": "^7.3.0",
     "concurrently": "^7.3.0",
     "cpx2": "^4.2.0",
     "cpx2": "^4.2.0",
     "crypto-browserify": "^3.12.0",
     "crypto-browserify": "^3.12.0",
     "css-loader": "^6.7.1",
     "css-loader": "^6.7.1",
-    "eslint": "^8.20.0",
+    "eslint": "^8.22.0",
     "extra-watch-webpack-plugin": "^1.0.3",
     "extra-watch-webpack-plugin": "^1.0.3",
     "file-loader": "^6.2.0",
     "file-loader": "^6.2.0",
     "fs-extra": "^10.1.0",
     "fs-extra": "^10.1.0",
-    "graphql": "^16.5.0",
+    "graphql": "^16.6.0",
     "http-server": "^14.1.1",
     "http-server": "^14.1.1",
     "jest": "^28.1.3",
     "jest": "^28.1.3",
     "mini-css-extract-plugin": "^2.6.1",
     "mini-css-extract-plugin": "^2.6.1",
@@ -123,14 +126,14 @@
     "raw-loader": "^4.0.2",
     "raw-loader": "^4.0.2",
     "react": "^18.2.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-dom": "^18.2.0",
-    "sass": "^1.54.0",
+    "sass": "^1.54.5",
     "sass-loader": "^13.0.2",
     "sass-loader": "^13.0.2",
-    "simple-git": "^3.10.0",
+    "simple-git": "^3.12.0",
     "stream-browserify": "^3.0.0",
     "stream-browserify": "^3.0.0",
     "style-loader": "^3.3.1",
     "style-loader": "^3.3.1",
-    "ts-jest": "^28.0.7",
+    "ts-jest": "^28.0.8",
     "typescript": "^4.7.4",
     "typescript": "^4.7.4",
-    "webpack": "^5.73.0",
+    "webpack": "^5.74.0",
     "webpack-cli": "^4.10.0"
     "webpack-cli": "^4.10.0"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -138,7 +141,7 @@
     "@types/benchmark": "^2.1.1",
     "@types/benchmark": "^2.1.1",
     "@types/compression": "1.7.2",
     "@types/compression": "1.7.2",
     "@types/express": "^4.17.13",
     "@types/express": "^4.17.13",
-    "@types/node": "^16.11.45",
+    "@types/node": "^16.11.51",
     "@types/node-fetch": "^2.6.2",
     "@types/node-fetch": "^2.6.2",
     "@types/swagger-ui-dist": "3.30.1",
     "@types/swagger-ui-dist": "3.30.1",
     "argparse": "^2.0.1",
     "argparse": "^2.0.1",
@@ -151,7 +154,7 @@
     "immutable": "^4.1.0",
     "immutable": "^4.1.0",
     "node-fetch": "^2.6.7",
     "node-fetch": "^2.6.7",
     "rxjs": "^7.5.6",
     "rxjs": "^7.5.6",
-    "swagger-ui-dist": "^4.13.0",
+    "swagger-ui-dist": "^4.14.0",
     "tslib": "^2.4.0",
     "tslib": "^2.4.0",
     "util.promisify": "^1.1.1",
     "util.promisify": "^1.1.1",
     "xhr2": "^0.2.1"
     "xhr2": "^0.2.1"

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

@@ -166,7 +166,7 @@ class Viewer {
             structures.push({ ref: structureProperties?.ref || structure.ref });
             structures.push({ ref: structureProperties?.ref || structure.ref });
         }
         }
 
 
-        // remove current structuresfrom hierarchy as they will be merged
+        // remove current structures from hierarchy as they will be merged
         // TODO only works with using loadStructuresFromUrlsAndMerge once
         // TODO only works with using loadStructuresFromUrlsAndMerge once
         //      need some more API metho to work with the hierarchy
         //      need some more API metho to work with the hierarchy
         this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');
         this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');

+ 3 - 3
src/apps/docking-viewer/viewport.tsx

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -202,14 +202,14 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
         const components = {
         const components = {
             ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
             ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
             surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
             surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
-            interactions: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `interactions`)
+            interactions: await presetStaticComponent(plugin, structureCell, 'ligand'),
         };
         };
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
             ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
             ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
-            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
+            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial, includeParent: true, parentDisplay: 'between' }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
             label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
             label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
         };
         };
 
 

+ 4 - 1
src/apps/viewer/app.ts

@@ -46,6 +46,7 @@ import { Color } from '../../mol-util/color';
 import '../../mol-util/polyfill';
 import '../../mol-util/polyfill';
 import { ObjectKeys } from '../../mol-util/type-helpers';
 import { ObjectKeys } from '../../mol-util/type-helpers';
 import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
 import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
+import { Backgrounds } from '../../extensions/backgrounds';
 
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
 export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
@@ -55,6 +56,7 @@ const CustomFormats = [
 ];
 ];
 
 
 const Extensions = {
 const Extensions = {
+    'backgrounds': PluginSpec.Behavior(Backgrounds),
     'cellpack': PluginSpec.Behavior(CellPack),
     'cellpack': PluginSpec.Behavior(CellPack),
     'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
     'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
     'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
     'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
@@ -197,7 +199,7 @@ export class Viewer {
         return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
         return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
     }
     }
 
 
-    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
+    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions & { label?: string }) {
         const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
         const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
             source: {
             source: {
@@ -206,6 +208,7 @@ export class Viewer {
                     url: Asset.Url(url),
                     url: Asset.Url(url),
                     format: format as any,
                     format: format as any,
                     isBinary,
                     isBinary,
+                    label: options?.label,
                     options: { ...params.source.params.options, representationParams: options?.representationParams as any },
                     options: { ...params.source.params.options, representationParams: options?.representationParams as any },
                 }
                 }
             }
             }

+ 9 - 0
src/apps/viewer/embedded.html

@@ -38,6 +38,15 @@
                 viewer.loadPdb('7bv2');
                 viewer.loadPdb('7bv2');
                 viewer.loadEmdb('EMD-30210', { detail: 6 });
                 viewer.loadEmdb('EMD-30210', { detail: 6 });
                 // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
                 // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
+                // viewer.loadStructureFromUrl('my url', 'pdb', false, {
+                //     representationParams: {
+                //         theme: {
+                //             globalName: 'uniform',
+                //             globalColorParams: { value: 0xff0000 }
+                //         }
+                //     },
+                //     label: 'my structure'
+                // });
             });
             });
         </script>
         </script>
     </body>
     </body>

BIN
src/extensions/backgrounds/images/cells.jpg


+ 90 - 0
src/extensions/backgrounds/index.ts

@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { PluginConfig } from '../../mol-plugin/config';
+import { Color } from '../../mol-util/color/color';
+
+// from https://visualsonline.cancer.gov/details.cfm?imageid=2304, public domain
+import image_cells from './images/cells.jpg';
+
+// created with http://alexcpeterson.com/spacescape/
+import face_nebula_nx from './skyboxes/nebula/nebula_left2.jpg';
+import face_nebula_ny from './skyboxes/nebula/nebula_bottom4.jpg';
+import face_nebula_nz from './skyboxes/nebula/nebula_back6.jpg';
+import face_nebula_px from './skyboxes/nebula/nebula_right1.jpg';
+import face_nebula_py from './skyboxes/nebula/nebula_top3.jpg';
+import face_nebula_pz from './skyboxes/nebula/nebula_front5.jpg';
+
+export const Backgrounds = PluginBehavior.create<{ }>({
+    name: 'extension-backgrounds',
+    category: 'misc',
+    display: {
+        name: 'Backgrounds'
+    },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        register(): void {
+            this.ctx.config.set(PluginConfig.Background.Styles, [
+                [{
+                    variant: {
+                        name: 'radialGradient',
+                        params: {
+                            centerColor: Color(0xFFFFFF),
+                            edgeColor: Color(0x808080),
+                            ratio: 0.2,
+                            coverage: 'viewport',
+                        }
+                    }
+                }, 'Light Radial Gradient'],
+                [{
+                    variant: {
+                        name: 'image',
+                        params: {
+                            source: {
+                                name: 'url',
+                                params: image_cells
+                            },
+                            lightness: 0,
+                            saturation: 0,
+                            opacity: 1,
+                            coverage: 'viewport',
+                        }
+                    }
+                }, 'Normal Cells Image'],
+                [{
+                    variant: {
+                        name: 'skybox',
+                        params: {
+                            faces: {
+                                name: 'urls',
+                                params: {
+                                    nx: face_nebula_nx,
+                                    ny: face_nebula_ny,
+                                    nz: face_nebula_nz,
+                                    px: face_nebula_px,
+                                    py: face_nebula_py,
+                                    pz: face_nebula_pz,
+                                }
+                            },
+                            lightness: 0,
+                            saturation: 0,
+                            opacity: 1,
+                        }
+                    }
+                }, 'Purple Nebula Skybox'],
+            ]);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.config.set(PluginConfig.Background.Styles, []);
+        }
+    },
+    params: () => ({ })
+});

BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg


BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg


BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg


BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg


BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg


BIN
src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg


+ 10 - 0
src/extensions/backgrounds/typings.d.ts

@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+declare module '*.jpg' {
+    const value: string;
+    export = value;
+}

+ 25 - 17
src/extensions/dnatco/confal-pyramids/property.ts

@@ -16,7 +16,7 @@ import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 
 
-export type ConfalPyramids = PropertyWrapper<CPT.StepsData | undefined>;
+export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
 
 
 export namespace ConfalPyramids {
 export namespace ConfalPyramids {
     export const Schema = {
     export const Schema = {
@@ -105,13 +105,13 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
 
 
 type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
 type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
 
 
-function createPyramidsFromCif(model: Model,
+function createPyramidsFromCif(
+    model: Model,
     cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
     cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
-    stepsSummary: StepsSummaryTable): CPT.StepsData {
+    stepsSummary: StepsSummaryTable
+): CPT.Steps {
     const steps = new Array<CPT.Step>();
     const steps = new Array<CPT.Step>();
-    const names = new Map<string, number>();
-    const halfPyramids = new Array<CPT.HalfPyramid>();
-    let hasMultipleModels = false;
+    const mapping = new Array<CPT.MappedChains>();
 
 
     const {
     const {
         id, PDB_model_number, name,
         id, PDB_model_number, name,
@@ -123,21 +123,24 @@ function createPyramidsFromCif(model: Model,
     if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
     if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
 
 
     for (let i = 0; i < _rowCount; i++) {
     for (let i = 0; i < _rowCount; i++) {
-        const model_num = PDB_model_number.value(i);
-        if (model_num !== model.modelNum)
-            hasMultipleModels = true;
-
         const {
         const {
             NtC,
             NtC,
             confal_score,
             confal_score,
             rmsd
             rmsd
         } = getSummaryData(id.value(i), i, stepsSummary);
         } = getSummaryData(id.value(i), i, stepsSummary);
+        const modelNum = PDB_model_number.value(i);
+        const chainId = auth_asym_id_1.value(i);
+        const seqId = auth_seq_id_1.value(i);
+        const modelIdx = modelNum - 1;
+
+        if (mapping.length <= modelIdx || !mapping[modelIdx])
+            mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
 
 
         const step = {
         const step = {
-            PDB_model_number: model_num,
+            PDB_model_number: modelNum,
             name: name.value(i),
             name: name.value(i),
-            auth_asym_id_1: auth_asym_id_1.value(i),
-            auth_seq_id_1: auth_seq_id_1.value(i),
+            auth_asym_id_1: chainId,
+            auth_seq_id_1: seqId,
             label_comp_id_1: label_comp_id_1.value(i),
             label_comp_id_1: label_comp_id_1.value(i),
             label_alt_id_1: label_alt_id_1.value(i),
             label_alt_id_1: label_alt_id_1.value(i),
             PDB_ins_code_1: PDB_ins_code_1.value(i),
             PDB_ins_code_1: PDB_ins_code_1.value(i),
@@ -152,13 +155,18 @@ function createPyramidsFromCif(model: Model,
         };
         };
 
 
         steps.push(step);
         steps.push(step);
-        names.set(step.name, steps.length - 1);
 
 
-        halfPyramids.push({ step, isLower: false });
-        halfPyramids.push({ step, isLower: true });
+        const mappedChains = mapping[modelIdx];
+        const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
+        const stepsForResidue = residuesOnChain.get(seqId) ?? [];
+        stepsForResidue.push(steps.length - 1);
+
+        residuesOnChain.set(seqId, stepsForResidue);
+        mappedChains.set(chainId, residuesOnChain);
+        mapping[modelIdx] = mappedChains;
     }
     }
 
 
-    return { steps, names, halfPyramids, hasMultipleModels };
+    return { steps, mapping };
 }
 }
 
 
 function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
 function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {

+ 74 - 54
src/extensions/dnatco/confal-pyramids/representation.ts

@@ -6,7 +6,7 @@
  */
  */
 
 
 import { ConfalPyramids, ConfalPyramidsProvider } from './property';
 import { ConfalPyramids, ConfalPyramidsProvider } from './property';
-import { ConfalPyramidsUtil } from './util';
+import { ConfalPyramidsIterator } from './util';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { Interval } from '../../../mol-data/int';
 import { Interval } from '../../../mol-data/int';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
@@ -32,6 +32,12 @@ const t = Mat4.identity();
 const w = Vec3.zero();
 const w = Vec3.zero();
 const mp = Vec3.zero();
 const mp = Vec3.zero();
 
 
+const posO3 = Vec3();
+const posP = Vec3();
+const posOP1 = Vec3();
+const posOP2 = Vec3();
+const posO5 = Vec3();
+
 function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
 function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
     Vec3.sub(mp, v, w);
     Vec3.sub(mp, v, w);
     Vec3.scale(mp, mp, 0.5);
     Vec3.scale(mp, mp, 0.5);
@@ -53,64 +59,76 @@ function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationI
     const { structure, group } = structureGroup;
     const { structure, group } = structureGroup;
     const instanceCount = group.units.length;
     const instanceCount = group.units.length;
 
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) {
-        return LocationIterator(0, 1, 1, () => NullLocation);
-    }
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
 
 
-    const { halfPyramids } = prop.data;
+    const halfPyramidsCount = data.steps.length * 2;
 
 
     const getLocation = (groupIndex: number, instanceIndex: number) => {
     const getLocation = (groupIndex: number, instanceIndex: number) => {
-        if (halfPyramids.length <= groupIndex) return NullLocation;
-        return CPT.Location(halfPyramids[groupIndex]);
+        if (halfPyramidsCount <= groupIndex) return NullLocation;
+        const idx = Math.floor(groupIndex / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
+        return CPT.Location(data.steps[idx], groupIndex % 2 === 1);
     };
     };
-    return LocationIterator(halfPyramids.length, instanceCount, 1, getLocation);
+    return LocationIterator(halfPyramidsCount, instanceCount, 1, getLocation);
 }
 }
 
 
 function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
 function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
 
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return Mesh.createEmpty(mesh);
 
 
-    const { steps } = prop.data;
+    const { steps, mapping } = data;
     if (steps.length === 0) return Mesh.createEmpty(mesh);
     if (steps.length === 0) return Mesh.createEmpty(mesh);
-
-    const mb = MeshBuilder.createState(512, 512, mesh);
-
-    const handler = (step: CPT.Step, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => {
-        if (firsLocIndex === -1 || secondLocIndex === -1)
-            throw new Error('Invalid location index');
-
-        const scale = (step.confal_score - 20.0) / 100.0;
-        const O3 = first.O3.pos;
-        const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos;
-
-        shiftVertex(O3, P, scale);
-        shiftVertex(OP1, P, scale);
-        shiftVertex(OP2, P, scale);
-        shiftVertex(O5, P, scale);
-        calcMidpoint(mp, O3, O5);
-
-        mb.currentGroup = firsLocIndex;
-        let pb = PrimitiveBuilder(3);
-        /* Upper part (for first residue in step) */
-        pb.add(O3, OP1, OP2);
-        pb.add(O3, mp, OP1);
-        pb.add(O3, OP2, mp);
-        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
-
-        /* Lower part (for second residue in step */
-        mb.currentGroup = secondLocIndex;
-        pb = PrimitiveBuilder(3);
-        pb.add(mp, O5, OP1);
-        pb.add(mp, OP2, O5);
-        pb.add(O5, OP2, OP1);
-        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
-    };
-
-    const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler);
-    walker.walk();
+    const vertexCount = (6 * steps.length) / mapping.length;
+
+    const mb = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
+
+    const it = new ConfalPyramidsIterator(structure, unit);
+    while (it.hasNext) {
+        const allPoints = it.move();
+
+        for (const points of allPoints) {
+            const { O3, P, OP1, OP2, O5, confalScore } = points;
+            const scale = (confalScore - 20.0) / 100.0;
+            // Steps can be drawn in a different order than they are stored.
+            // To make sure that we can get from the drawn pyramid back to the step in represents,
+            // we need to use an appropriate groupId. The stepIdx passed from the iterator
+            // is an index into the array of all steps in the structure.
+            // Since a step is drawn as two "half-pyramids" we need two ids to map to a single step.
+            // To do that, we just multiply the index by 2. idx*2 marks the "upper" half-pyramid,
+            // (idx*2)+1 the "lower" half-pyramid.
+            const groupIdx = points.stepIdx * 2;
+
+            unit.conformation.invariantPosition(O3, posO3);
+            unit.conformation.invariantPosition(P, posP);
+            unit.conformation.invariantPosition(OP1, posOP1);
+            unit.conformation.invariantPosition(OP2, posOP2);
+            unit.conformation.invariantPosition(O5, posO5);
+
+            shiftVertex(posO3, posP, scale);
+            shiftVertex(posOP1, posP, scale);
+            shiftVertex(posOP2, posP, scale);
+            shiftVertex(posO5, posP, scale);
+            calcMidpoint(mp, posO3, posO5);
+
+            mb.currentGroup = groupIdx;
+            let pb = PrimitiveBuilder(3);
+            /* Upper part (for first residue in step) */
+            pb.add(posO3, posOP1, posOP2);
+            pb.add(posO3, mp, posOP1);
+            pb.add(posO3, posOP2, mp);
+            MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+
+            /* Lower part (for second residue in step) */
+            mb.currentGroup = groupIdx + 1;
+            pb = PrimitiveBuilder(3);
+            pb.add(mp, posO5, posOP1);
+            pb.add(mp, posOP2, posO5);
+            pb.add(posO5, posOP2, posOP1);
+            MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+        }
+    }
 
 
     return MeshBuilder.getMesh(mb);
     return MeshBuilder.getMesh(mb);
 }
 }
@@ -124,15 +142,17 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
     const unit = structureGroup.group.units[instanceId];
     const unit = structureGroup.group.units[instanceId];
     if (!Unit.isAtomic(unit)) return EmptyLoci;
     if (!Unit.isAtomic(unit)) return EmptyLoci;
 
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) return EmptyLoci;
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return EmptyLoci;
+
+    const halfPyramidsCount = data.steps.length * 2;
 
 
-    const { halfPyramids } = prop.data;
+    if (halfPyramidsCount <= groupId) return EmptyLoci;
 
 
-    if (halfPyramids.length <= groupId) return EmptyLoci;
-    const hp = halfPyramids[groupId];
+    const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
+    const step = data.steps[idx];
 
 
-    return CPT.Loci(hp, [{}]);
+    return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
 }
 }
 
 
 function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
 function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {

+ 7 - 6
src/extensions/dnatco/confal-pyramids/types.ts

@@ -30,11 +30,12 @@ export namespace ConfalPyramidsTypes {
         rmsd: number,
         rmsd: number,
     }
     }
 
 
-    export interface StepsData {
+    export type MappedChains = Map<string, MappedResidues>;
+    export type MappedResidues = Map<number, number[]>;
+
+    export interface Steps {
         steps: Array<Step>,
         steps: Array<Step>,
-        names: Map<string, number>,
-        halfPyramids: Array<HalfPyramid>,
-        hasMultipleModels: boolean
+        mapping: MappedChains[],
     }
     }
 
 
     export interface HalfPyramid {
     export interface HalfPyramid {
@@ -44,8 +45,8 @@ export namespace ConfalPyramidsTypes {
 
 
     export interface Location extends DataLocation<HalfPyramid, {}> {}
     export interface Location extends DataLocation<HalfPyramid, {}> {}
 
 
-    export function Location(halfPyramid: HalfPyramid) {
-        return DataLocation(DataTag, halfPyramid, {});
+    export function Location(step: Step, isLower: boolean) {
+        return DataLocation(DataTag, { step, isLower }, {});
     }
     }
 
 
     export function isLocation(x: any): x is Location {
     export function isLocation(x: any): x is Location {

+ 87 - 247
src/extensions/dnatco/confal-pyramids/util.ts

@@ -8,280 +8,120 @@
 import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { Segmentation } from '../../../mol-data/int';
 import { Segmentation } from '../../../mol-data/int';
-import { Vec3 } from '../../../mol-math/linear-algebra';
 import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
 import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
 
 
-export namespace ConfalPyramidsUtil {
-    type Residue = Segmentation.Segment<ResidueIndex>;
+type Residue = Segmentation.Segment<ResidueIndex>;
 
 
-    export type AtomInfo = {
-        pos: Vec3,
-        index: ElementIndex,
-        fakeAltId: string,
-    };
+export type Pyramid = {
+    O3: ElementIndex,
+    P: ElementIndex,
+    OP1: ElementIndex,
+    OP2: ElementIndex,
+    O5: ElementIndex,
+    confalScore: number,
+    stepIdx: number,
+};
 
 
-    export type FirstResidueAtoms = {
-        O3: AtomInfo,
-    };
+const EmptyStepIndices = new Array<number>();
 
 
-    export type SecondResidueAtoms = {
-        OP1: AtomInfo,
-        OP2: AtomInfo,
-        O5: AtomInfo,
-        P: AtomInfo,
-    };
-
-    type ResidueInfo = {
-        PDB_model_num: number,
-        asym_id: string,
-        auth_asym_id: string,
-        seq_id: number,
-        auth_seq_id: number,
-        comp_id: string,
-        alt_id: string,
-        ins_code: string,
-    };
+function copyResidue(r?: Residue) {
+    return r ? { index: r.index, start: r.start, end: r.end } : void 0;
+}
 
 
-    export type Handler = (pyramid: CPT.Step, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void;
+function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
+    for (let eI = residue.start; eI < residue.end; eI++) {
+        loc.element = loc.unit.elements[eI];
+        const elName = StructureProperties.atom.label_atom_id(loc);
+        const elAltId = StructureProperties.atom.label_alt_id(loc);
 
 
-    function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo {
-        return {
-            PDB_model_num: StructureProperties.unit.model_num(loc),
-            asym_id: StructureProperties.chain.label_asym_id(loc),
-            auth_asym_id: StructureProperties.chain.auth_asym_id(loc),
-            seq_id: StructureProperties.residue.label_seq_id(loc),
-            auth_seq_id: StructureProperties.residue.auth_seq_id(loc),
-            comp_id: StructureProperties.atom.label_comp_id(loc),
-            alt_id: StructureProperties.atom.label_alt_id(loc),
-            ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc)
-        };
+        if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
+            return loc.element;
     }
     }
 
 
-    export function hasMultipleModels(unit: Unit.Atomic): boolean {
-        const prop = ConfalPyramidsProvider.get(unit.model).value;
-        if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
-        return prop.data.hasMultipleModels;
-    }
+    return -1 as ElementIndex;
+}
 
 
-    function getPossibleAltIds(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] {
-        const possibleAltIds: string[] = [];
+function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
+    const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
+    const P = getAtomIndex(loc, two, ['P'], altIdTwo);
+    const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
+    const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
+    const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
 
 
-        const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
-        for (let rI = residue.start; rI <= residue.end - 1; rI++) {
-            loc.element = unit.elements[rI];
-            const altId = StructureProperties.atom.label_alt_id(loc);
-            if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId);
-        }
+    return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
+}
 
 
-        return possibleAltIds;
+export class ConfalPyramidsIterator {
+    private chainIt: Segmentation.SegmentIterator<ChainIndex>;
+    private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
+    private residueOne?: Residue;
+    private residueTwo: Residue;
+    private data?: CPT.Steps;
+    private loc: StructureElement.Location;
+
+    private getStepIndices(r: Residue) {
+        this.loc.element = this.loc.unit.elements[r.start];
+
+        const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
+        const chainId = StructureProperties.chain.auth_asym_id(this.loc);
+        const seqId = StructureProperties.residue.auth_seq_id(this.loc);
+
+        const chains = this.data!.mapping[modelIdx];
+        if (!chains) return EmptyStepIndices;
+        const residues = chains.get(chainId);
+        if (!residues) return EmptyStepIndices;
+        return residues.get(seqId) ?? EmptyStepIndices;
     }
     }
 
 
-    class Utility {
-        protected getPyramidByName(name: string): { pyramid: CPT.Step | undefined, index: number } {
-            const index = this.data.names.get(name);
-            if (index === undefined) return { pyramid: undefined, index: -1 };
-
-            return { pyramid: this.data.steps[index], index };
-        }
-
-        protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) {
-            const first = residueInfoFromLocation(locFirst);
-            const second = residueInfoFromLocation(locSecond);
-            const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
-            const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
-            const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
-            const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
-            const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';
-
-            return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`;
-        }
-
-        constructor(unit: Unit.Atomic) {
-            const prop = ConfalPyramidsProvider.get(unit.model).value;
-            if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
-
-            this.data = prop.data;
-            this.hasMultipleModels = hasMultipleModels(unit);
+    private moveStep() {
+        this.residueOne = copyResidue(this.residueTwo);
+        this.residueTwo = copyResidue(this.residueIt.move())!;
 
 
-            this.entryId = unit.model.entryId.toLowerCase();
-            this.modelNum = unit.model.modelNum;
-        }
-
-        protected readonly data: CPT.StepsData;
-        protected readonly hasMultipleModels: boolean;
-        protected readonly entryId: string;
-        protected readonly modelNum: number;
+        return this.toPyramids(this.residueOne!, this.residueTwo);
     }
     }
 
 
-    export class UnitWalker extends Utility {
-        private getAtomIndices(names: string[], residue: Residue): ElementIndex[] {
-            const indices: ElementIndex[] = [];
-
-            const loc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            for (let rI = residue.start; rI <= residue.end - 1; rI++) {
-                loc.element = this.unit.elements[rI];
-                const thisName = StructureProperties.atom.label_atom_id(loc);
-                if (names.includes(thisName)) indices.push(loc.element);
-            }
-
-            if (indices.length === 0) {
-                let namesStr = '';
-                for (const n of names)
-                    namesStr += `${n} `;
+    private toPyramids(one: Residue, two: Residue) {
+        const indices = this.getStepIndices(one);
 
 
-                throw new Error(`Element [${namesStr}] not found on residue ${residue.index}`);
-            }
-
-            return indices;
+        const points = [];
+        for (const idx of indices) {
+            const step = this.data!.steps[idx];
+            points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
         }
         }
 
 
-        private getAtomPositions(indices: ElementIndex[]): Vec3[] {
-            const pos = this.unit.conformation.invariantPosition;
-            const positions: Vec3[] = [];
-
-            for (const eI of indices) {
-                const v = Vec3.zero();
-                pos(eI, v);
-                positions.push(v);
-            }
-
-            return positions;
-        }
-
-        private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) {
-            const modelNum = this.hasMultipleModels ? this.modelNum : -1;
-            let ok = false;
-
-            const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            for (let i = 0; i < firstAtoms.length; i++) {
-                const first = firstAtoms[i];
-                for (let j = 0; j < secondAtoms.length; j++) {
-                    const second = secondAtoms[j];
-                    firstLoc.element = first.O3.index;
-                    secondLoc.element = second.OP1.index;
-
-                    const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId);
-                    const { pyramid, index } = this.getPyramidByName(name);
-                    if (pyramid !== undefined) {
-                        const locIndex = index * 2;
-                        this.handler(pyramid, first, second, locIndex, locIndex + 1);
-                        ok = true;
-                    }
-                }
-            }
-
-            if (!ok) throw new Error('Bogus step');
-        }
-
-        private processFirstResidue(residue: Residue, possibleAltIds: string[]) {
-            const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue);
-            const posO3 = this.getAtomPositions(indO3);
-
-            const altPos: FirstResidueAtoms[] = [
-                { O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } }
-            ];
-
-            for (let i = 1; i < indO3.length; i++) {
-                altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } });
-            }
-
-            if (altPos.length === 1 && possibleAltIds.length > 1) {
-                /* We have some alternate positions on the residue but O3 does not have any - fake them */
-                altPos[0].O3.fakeAltId = possibleAltIds[0];
-
-                for (let i = 1; i < possibleAltIds.length; i++)
-                    altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } });
-            }
-
-            return altPos;
-        }
-
-        private processSecondResidue(residue: Residue, possibleAltIds: string[]) {
-            const indOP1 = this.getAtomIndices(['OP1'], residue);
-            const indOP2 = this.getAtomIndices(['OP2'], residue);
-            const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue);
-            const indP = this.getAtomIndices(['P'], residue);
-
-            const posOP1 = this.getAtomPositions(indOP1);
-            const posOP2 = this.getAtomPositions(indOP2);
-            const posO5 = this.getAtomPositions(indO5);
-            const posP = this.getAtomPositions(indP);
-
-            const infoOP1: AtomInfo[] = [];
-            /* We use OP1 as "pivotal" atom. There is no specific reason
-             * to pick OP1, it is as good a choice as any other atom
-             */
-            if (indOP1.length === 1 && possibleAltIds.length > 1) {
-                /* No altIds on OP1, fake them */
-                for (const altId of possibleAltIds)
-                    infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId });
-            } else {
-                for (let i = 0; i < indOP1.length; i++)
-                    infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' });
-            }
-
-            const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => {
-                if (i >= indices.length) {
-                    const last = indices.length - 1;
-                    return { pos: positions[last], index: indices[last], fakeAltId: altId };
-                }
-
-                return { pos: positions[i], index: indices[i], fakeAltId: altId };
-            };
-
-            const altPos: SecondResidueAtoms[] = [];
-            for (let i = 0; i < infoOP1.length; i++) {
-                const altId = infoOP1[i].fakeAltId;
-
-                const OP2 = mkInfo(i, indOP2, posOP2, altId);
-                const O5 = mkInfo(i, indO5, posO5, altId);
-                const P = mkInfo(i, indP, posP, altId);
-
-                altPos.push({ OP1: infoOP1[i], OP2, O5, P });
-            }
-
-            return altPos;
-        }
-
-        private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } {
-            const firstPossibleAltIds = getPossibleAltIds(residue, this.structure, this.unit);
-            const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds);
+        return points;
+    }
 
 
-            residue = this.residueIt.move();
+    constructor(structure: Structure, unit: Unit) {
+        this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
+        this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
 
 
-            const secondPossibleAltIds = getPossibleAltIds(residue, this.structure, this.unit);
-            const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds);
+        const prop = ConfalPyramidsProvider.get(unit.model).value;
+        this.data = prop?.data;
 
 
-            return { firstAtoms, secondAtoms };
+        if (this.chainIt.hasNext) {
+            this.residueIt.setSegment(this.chainIt.move());
+            if (this.residueIt.hasNext)
+                this.residueTwo = this.residueIt.move();
         }
         }
 
 
-        walk() {
-            while (this.chainIt.hasNext) {
-                this.residueIt.setSegment(this.chainIt.move());
-
-                let residue = this.residueIt.move();
-                while (this.residueIt.hasNext) {
-                    try {
-                        const { firstAtoms, secondAtoms } = this.step(residue);
-
-                        this.handleStep(firstAtoms, secondAtoms);
-                    } catch (error) {
-                        /* Skip and move along */
-                        residue = this.residueIt.move();
-                    }
-                }
-            }
-        }
+        this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
+    }
 
 
-        constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) {
-            super(unit);
+    get hasNext() {
+        if (!this.data)
+            return false;
+        return this.residueIt.hasNext
+            ? true
+            : this.chainIt.hasNext;
+    }
 
 
-            this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
-            this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+    move() {
+        if (this.residueIt.hasNext) {
+            return this.moveStep();
+        } else {
+            this.residueIt.setSegment(this.chainIt.move());
+            return this.moveStep();
         }
         }
-
-        private chainIt: Segmentation.SegmentIterator<ChainIndex>;
-        private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
     }
     }
 }
 }

+ 1 - 0
src/extensions/mp4-export/encoder.ts

@@ -69,6 +69,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
         const dt = durationMs / N;
         const dt = durationMs / N;
 
 
         await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
         await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
+        await params.pass.updateBackground();
 
 
         await plugin.managers.animation.play(params.animation.definition, params.animation.params);
         await plugin.managers.animation.play(params.animation.definition, params.animation.params);
         stoppedAnimation = false;
         stoppedAnimation = false;

+ 1 - 4
src/extensions/rcsb/graphql/types.ts

@@ -4,7 +4,7 @@ export type InputMaybe<T> = Maybe<T>;
 export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
 export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
 export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
 export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
 export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
 export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
-// Generated on 2022-06-26T14:02:35-07:00
+// Generated on 2022-08-20T16:36:05-07:00
 
 
 /** All built-in and custom scalars, mapped to their actual values */
 /** All built-in and custom scalars, mapped to their actual values */
 export type Scalars = {
 export type Scalars = {
@@ -13,11 +13,8 @@ export type Scalars = {
   Boolean: boolean;
   Boolean: boolean;
   Int: number;
   Int: number;
   Float: number;
   Float: number;
-  /** Built-in scalar representing an instant in time */
   Date: any;
   Date: any;
-  /** Built-in scalar for dynamic values */
   ObjectScalar: any;
   ObjectScalar: any;
-  /** Use SPQR's SchemaPrinter to remove this from SDL */
   UNREPRESENTABLE: any;
   UNREPRESENTABLE: any;
 };
 };
 
 

+ 43 - 10
src/mol-canvas3d/canvas3d.ts

@@ -40,6 +40,9 @@ import { Passes } from './passes/passes';
 import { shallowEqual } from '../mol-util';
 import { shallowEqual } from '../mol-util';
 import { MarkingParams } from './passes/marking';
 import { MarkingParams } from './passes/marking';
 import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item';
 import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item';
+import { degToRad, radToDeg } from '../mol-math/misc';
+import { AssetManager } from '../mol-util/assets';
+import { deepClone } from '../mol-util/object';
 
 
 export const Canvas3DParams = {
 export const Canvas3DParams = {
     camera: PD.Group({
     camera: PD.Group({
@@ -49,6 +52,7 @@ export const Canvas3DParams = {
             on: PD.Group(StereoCameraParams),
             on: PD.Group(StereoCameraParams),
             off: PD.Group({})
             off: PD.Group({})
         }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
         }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
+        fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
         manualReset: PD.Boolean(false, { isHidden: true }),
         manualReset: PD.Boolean(false, { isHidden: true }),
     }, { pivot: 'mode' }),
     }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
     cameraFog: PD.MappedStatic('on', {
@@ -78,6 +82,7 @@ export const Canvas3DParams = {
     }),
     }),
 
 
     cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
     cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
+    sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
     transparentBackground: PD.Boolean(false),
     transparentBackground: PD.Boolean(false),
 
 
     multiSample: PD.Group(MultiSampleParams),
     multiSample: PD.Group(MultiSampleParams),
@@ -106,6 +111,7 @@ interface Canvas3DContext {
     readonly attribs: Readonly<Canvas3DContext.Attribs>
     readonly attribs: Readonly<Canvas3DContext.Attribs>
     readonly contextLost: BehaviorSubject<now.Timestamp>
     readonly contextLost: BehaviorSubject<now.Timestamp>
     readonly contextRestored: BehaviorSubject<now.Timestamp>
     readonly contextRestored: BehaviorSubject<now.Timestamp>
+    readonly assetManager: AssetManager
     dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
     dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
 }
 }
 
 
@@ -124,7 +130,7 @@ namespace Canvas3DContext {
     };
     };
     export type Attribs = typeof DefaultAttribs
     export type Attribs = typeof DefaultAttribs
 
 
-    export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
+    export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
         const a = { ...DefaultAttribs, ...attribs };
         const a = { ...DefaultAttribs, ...attribs };
         const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const gl = getGLContext(canvas, {
         const gl = getGLContext(canvas, {
@@ -139,7 +145,7 @@ namespace Canvas3DContext {
 
 
         const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
         const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
         const webgl = createContext(gl, { pixelScale });
         const webgl = createContext(gl, { pixelScale });
-        const passes = new Passes(webgl, a);
+        const passes = new Passes(webgl, assetManager, a);
 
 
         if (isDebugMode) {
         if (isDebugMode) {
             const loseContextExt = gl.getExtension('WEBGL_lose_context');
             const loseContextExt = gl.getExtension('WEBGL_lose_context');
@@ -192,6 +198,7 @@ namespace Canvas3DContext {
             attribs: a,
             attribs: a,
             contextLost,
             contextLost,
             contextRestored: webgl.contextRestored,
             contextRestored: webgl.contextRestored,
+            assetManager,
             dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
             dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
                 input.dispose();
                 input.dispose();
 
 
@@ -278,8 +285,8 @@ namespace Canvas3D {
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
 
-    export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
-        const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
+    export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
+        const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
 
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
@@ -299,11 +306,16 @@ namespace Canvas3D {
 
 
         const scene = Scene.create(webgl, passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended);
         const scene = Scene.create(webgl, passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended);
 
 
+        function getSceneRadius() {
+            return scene.boundingSphere.radius * p.sceneRadiusFactor;
+        }
+
         const camera = new Camera({
         const camera = new Camera({
             position: Vec3.create(0, 0, 100),
             position: Vec3.create(0, 0, 100),
             mode: p.camera.mode,
             mode: p.camera.mode,
             fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
             fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
-            clipFar: p.cameraClipping.far
+            clipFar: p.cameraClipping.far,
+            fov: degToRad(p.camera.fov),
         }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
         }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
         const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
         const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
 
 
@@ -315,6 +327,10 @@ namespace Canvas3D {
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
         const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
         const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
 
 
+        passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
+            if (changed) requestDraw();
+        });
+
         let cameraResetRequested = false;
         let cameraResetRequested = false;
         let nextCameraResetDuration: number | undefined = void 0;
         let nextCameraResetDuration: number | undefined = void 0;
         let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
         let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
@@ -523,7 +539,7 @@ namespace Canvas3D {
                 const focus = camera.getFocus(center, radius);
                 const focus = camera.getFocus(center, radius);
                 const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
                 const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
                 const snapshot = next ? { ...focus, ...next } : focus;
                 const snapshot = next ? { ...focus, ...next } : focus;
-                camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
+                camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
             }
             }
 
 
             nextCameraResetDuration = void 0;
             nextCameraResetDuration = void 0;
@@ -574,7 +590,7 @@ namespace Canvas3D {
             }
             }
             if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
             if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
 
 
-            if (!p.camera.manualReset) camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
+            if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0);
             reprCount.next(reprRenderObjects.size);
             reprCount.next(reprRenderObjects.size);
             if (isDebugMode) consoleStats();
             if (isDebugMode) consoleStats();
 
 
@@ -650,7 +666,7 @@ namespace Canvas3D {
 
 
         function getProps(): Canvas3DProps {
         function getProps(): Canvas3DProps {
             const radius = scene.boundingSphere.radius > 0
             const radius = scene.boundingSphere.radius > 0
-                ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
+                ? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100)
                 : 0;
                 : 0;
 
 
             return {
             return {
@@ -658,6 +674,7 @@ namespace Canvas3D {
                     mode: camera.state.mode,
                     mode: camera.state.mode,
                     helper: { ...helper.camera.props },
                     helper: { ...helper.camera.props },
                     stereo: { ...p.camera.stereo },
                     stereo: { ...p.camera.stereo },
+                    fov: Math.round(radToDeg(camera.state.fov)),
                     manualReset: !!p.camera.manualReset
                     manualReset: !!p.camera.manualReset
                 },
                 },
                 cameraFog: camera.state.fog > 0
                 cameraFog: camera.state.fog > 0
@@ -665,6 +682,7 @@ namespace Canvas3D {
                     : { name: 'off' as const, params: {} },
                     : { name: 'off' as const, params: {} },
                 cameraClipping: { far: camera.state.clipFar, radius },
                 cameraClipping: { far: camera.state.clipFar, radius },
                 cameraResetDurationMs: p.cameraResetDurationMs,
                 cameraResetDurationMs: p.cameraResetDurationMs,
+                sceneRadiusFactor: p.sceneRadiusFactor,
                 transparentBackground: p.transparentBackground,
                 transparentBackground: p.transparentBackground,
                 viewport: p.viewport,
                 viewport: p.viewport,
 
 
@@ -767,10 +785,19 @@ namespace Canvas3D {
                     ? produce(getProps(), properties as any)
                     ? produce(getProps(), properties as any)
                     : properties;
                     : properties;
 
 
+                if (props.sceneRadiusFactor !== undefined) {
+                    p.sceneRadiusFactor = props.sceneRadiusFactor;
+                    camera.setState({ radiusMax: getSceneRadius() }, 0);
+                }
+
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
                 if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
                     cameraState.mode = props.camera.mode;
                     cameraState.mode = props.camera.mode;
                 }
                 }
+                const oldFov = Math.round(radToDeg(camera.state.fov));
+                if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
+                    cameraState.fov = degToRad(props.camera.fov);
+                }
                 if (props.cameraFog !== undefined && props.cameraFog.params) {
                 if (props.cameraFog !== undefined && props.cameraFog.params) {
                     const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
                     const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
                     if (newFog !== camera.state.fog) cameraState.fog = newFog;
                     if (newFog !== camera.state.fog) cameraState.fog = newFog;
@@ -780,7 +807,7 @@ namespace Canvas3D {
                         cameraState.clipFar = props.cameraClipping.far;
                         cameraState.clipFar = props.cameraClipping.far;
                     }
                     }
                     if (props.cameraClipping.radius !== undefined) {
                     if (props.cameraClipping.radius !== undefined) {
-                        const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius);
+                        const radius = (getSceneRadius() / 100) * (100 - props.cameraClipping.radius);
                         if (radius > 0 && radius !== cameraState.radius) {
                         if (radius > 0 && radius !== cameraState.radius) {
                             // if radius = 0, NaNs happen
                             // if radius = 0, NaNs happen
                             cameraState.radius = Math.max(radius, 0.01);
                             cameraState.radius = Math.max(radius, 0.01);
@@ -805,6 +832,12 @@ namespace Canvas3D {
                     }
                     }
                 }
                 }
 
 
+                if (props.postprocessing?.background) {
+                    Object.assign(p.postprocessing.background, props.postprocessing.background);
+                    passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
+                        if (changed && !doNotRequestDraw) requestDraw();
+                    });
+                }
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
                 if (props.marking) Object.assign(p.marking, props.marking);
                 if (props.marking) Object.assign(p.marking, props.marking);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
@@ -823,7 +856,7 @@ namespace Canvas3D {
                 }
                 }
             },
             },
             getImagePass: (props: Partial<ImageProps> = {}) => {
             getImagePass: (props: Partial<ImageProps> = {}) => {
-                return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
+                return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
             },
             },
             getRenderObjects(): GraphicsRenderObject[] {
             getRenderObjects(): GraphicsRenderObject[] {
                 const renderObjects: GraphicsRenderObject[] = [];
                 const renderObjects: GraphicsRenderObject[] = [];

+ 461 - 0
src/mol-canvas3d/passes/background.ts

@@ -0,0 +1,461 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadPositions, } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { AttributeSpec, DefineSpec, TextureSpec, UniformSpec, Values, ValueSpec } from '../../mol-gl/renderable/schema';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { background_frag } from '../../mol-gl/shader/background.frag';
+import { background_vert } from '../../mol-gl/shader/background.vert';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { createNullTexture, CubeFaces, Texture } from '../../mol-gl/webgl/texture';
+import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
+import { ValueCell } from '../../mol-util/value-cell';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { isTimingMode } from '../../mol-util/debug';
+import { Camera, ICamera } from '../camera';
+import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
+import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
+import { Color } from '../../mol-util/color';
+import { Asset, AssetManager } from '../../mol-util/assets';
+import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
+
+const SharedParams = {
+    opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
+    saturation: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }),
+    lightness: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }),
+};
+
+const SkyboxParams = {
+    faces: PD.MappedStatic('urls', {
+        urls: PD.Group({
+            nx: PD.Text('', { label: 'Negative X / Left' }),
+            ny: PD.Text('', { label: 'Negative Y / Bottom' }),
+            nz: PD.Text('', { label: 'Negative Z / Back' }),
+            px: PD.Text('', { label: 'Positive X / Right' }),
+            py: PD.Text('', { label: 'Positive Y / Top' }),
+            pz: PD.Text('', { label: 'Positive Z / Front' }),
+        }, { isExpanded: true, label: 'URLs' }),
+        files: PD.Group({
+            nx: PD.File({ label: 'Negative X / Left', accept: 'image/*' }),
+            ny: PD.File({ label: 'Negative Y / Bottom', accept: 'image/*' }),
+            nz: PD.File({ label: 'Negative Z / Back', accept: 'image/*' }),
+            px: PD.File({ label: 'Positive X / Right', accept: 'image/*' }),
+            py: PD.File({ label: 'Positive Y / Top', accept: 'image/*' }),
+            pz: PD.File({ label: 'Positive Z / Front', accept: 'image/*' }),
+        }, { isExpanded: true, label: 'Files' }),
+    }),
+    ...SharedParams,
+};
+type SkyboxProps = PD.Values<typeof SkyboxParams>
+
+const ImageParams = {
+    source: PD.MappedStatic('url', {
+        url: PD.Text(''),
+        file: PD.File({ accept: 'image/*' }),
+    }),
+    ...SharedParams,
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+type ImageProps = PD.Values<typeof ImageParams>
+
+const HorizontalGradientParams = {
+    topColor: PD.Color(Color(0xDDDDDD)),
+    bottomColor: PD.Color(Color(0xEEEEEE)),
+    ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+
+const RadialGradientParams = {
+    centerColor: PD.Color(Color(0xDDDDDD)),
+    edgeColor: PD.Color(Color(0xEEEEEE)),
+    ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+
+export const BackgroundParams = {
+    variant: PD.MappedStatic('off', {
+        off: PD.EmptyGroup(),
+        skybox: PD.Group(SkyboxParams, { isExpanded: true }),
+        image: PD.Group(ImageParams, { isExpanded: true }),
+        horizontalGradient: PD.Group(HorizontalGradientParams, { isExpanded: true }),
+        radialGradient: PD.Group(RadialGradientParams, { isExpanded: true }),
+    }, { label: 'Environment' }),
+};
+export type BackgroundProps = PD.Values<typeof BackgroundParams>
+
+export class BackgroundPass {
+    private renderable: BackgroundRenderable;
+
+    private skybox: {
+        texture: Texture
+        props: SkyboxProps
+        assets: Asset[]
+        loaded: boolean
+    } | undefined;
+
+    private image: {
+        texture: Texture
+        props: ImageProps
+        asset: Asset
+        loaded: boolean
+    } | undefined;
+
+    private readonly camera = new Camera();
+    private readonly target = Vec3();
+    private readonly position = Vec3();
+    private readonly dir = Vec3();
+
+    readonly texture: Texture;
+
+    constructor(private readonly webgl: WebGLContext, private readonly assetManager: AssetManager, width: number, height: number) {
+        this.renderable = getBackgroundRenderable(webgl, width, height);
+    }
+
+    setSize(width: number, height: number) {
+        const [w, h] = this.renderable.values.uTexSize.ref.value;
+
+        if (width !== w || height !== h) {
+            ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
+        }
+    }
+
+    private clearSkybox() {
+        if (this.skybox !== undefined) {
+            this.skybox.texture.destroy();
+            this.skybox.assets.forEach(a => this.assetManager.release(a));
+            this.skybox = undefined;
+        }
+    }
+
+    private updateSkybox(camera: ICamera, props: SkyboxProps, onload?: (changed: boolean) => void) {
+        const tf = this.skybox?.props.faces;
+        const f = props.faces.params;
+        if (!f.nx || !f.ny || !f.nz || !f.px || !f.py || !f.pz) {
+            this.clearSkybox();
+            onload?.(false);
+            return;
+        }
+        if (!this.skybox || !tf || !areSkyboxTexturePropsEqual(props.faces, this.skybox.props.faces)) {
+            this.clearSkybox();
+            const { texture, assets } = getSkyboxTexture(this.webgl, this.assetManager, props.faces, errored => {
+                if (this.skybox) this.skybox.loaded = !errored;
+                onload?.(true);
+            });
+            this.skybox = { texture, props: { ...props }, assets, loaded: false };
+            ValueCell.update(this.renderable.values.tSkybox, texture);
+            this.renderable.update();
+        } else {
+            onload?.(false);
+        }
+        if (!this.skybox) return;
+
+        let cam = camera;
+        if (camera.state.mode === 'orthographic') {
+            this.camera.setState({ ...camera.state, mode: 'perspective' });
+            this.camera.update();
+            cam = this.camera;
+        }
+
+        const m = this.renderable.values.uViewDirectionProjectionInverse.ref.value;
+        Vec3.sub(this.dir, cam.state.position, cam.state.target);
+        Vec3.setMagnitude(this.dir, this.dir, 0.1);
+        Vec3.copy(this.position, this.dir);
+        Mat4.lookAt(m, this.position, this.target, cam.state.up);
+        Mat4.mul(m, cam.projection, m);
+        Mat4.invert(m, m);
+        ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m);
+
+        ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
+        ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
+        ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, 'skybox');
+        this.renderable.update();
+    }
+
+    private clearImage() {
+        if (this.image !== undefined) {
+            this.image.texture.destroy();
+            this.assetManager.release(this.image.asset);
+            this.image = undefined;
+        }
+    }
+
+    private updateImage(props: ImageProps, onload?: (loaded: boolean) => void) {
+        if (!props.source.params) {
+            this.clearImage();
+            onload?.(false);
+            return;
+        }
+        if (!this.image || !this.image.props.source.params || !areImageTexturePropsEqual(props.source, this.image.props.source)) {
+            this.clearImage();
+            const { texture, asset } = getImageTexture(this.webgl, this.assetManager, props.source, errored => {
+                if (this.image) this.image.loaded = !errored;
+                onload?.(true);
+            });
+            this.image = { texture, props: { ...props }, asset, loaded: false };
+            ValueCell.update(this.renderable.values.tImage, texture);
+            this.renderable.update();
+        } else {
+            onload?.(false);
+        }
+        if (!this.image) return;
+
+        ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
+        ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
+        ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
+        ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, props.coverage === 'viewport' ? true : false);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, 'image');
+        this.renderable.update();
+    }
+
+    private updateImageScaling() {
+        const v = this.renderable.values;
+        const [w, h] = v.uTexSize.ref.value;
+        const iw = this.image?.texture.getWidth() || 0;
+        const ih = this.image?.texture.getHeight() || 0;
+        const r = w / h;
+        const ir = iw / ih;
+        // responsive scaling with offset
+        if (r < ir) {
+            ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, iw * h / ih, h));
+        } else {
+            ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, w, ih * w / iw));
+        }
+        const [rw, rh] = v.uImageScale.ref.value;
+        const sr = rw / rh;
+        if (sr > r) {
+            ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, (1 - r / sr) / 2, 0));
+        } else {
+            ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, 0, (1 - sr / r) / 2));
+        }
+    }
+
+    private updateGradient(colorA: Color, colorB: Color, ratio: number, variant: 'horizontalGradient' | 'radialGradient', viewportAdjusted: boolean) {
+        ValueCell.update(this.renderable.values.uGradientColorA, Color.toVec3Normalized(this.renderable.values.uGradientColorA.ref.value, colorA));
+        ValueCell.update(this.renderable.values.uGradientColorB, Color.toVec3Normalized(this.renderable.values.uGradientColorB.ref.value, colorB));
+        ValueCell.updateIfChanged(this.renderable.values.uGradientRatio, ratio);
+        ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, viewportAdjusted);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, variant);
+        this.renderable.update();
+    }
+
+    update(camera: ICamera, props: BackgroundProps, onload?: (changed: boolean) => void) {
+        if (props.variant.name === 'off') {
+            this.clearSkybox();
+            this.clearImage();
+            onload?.(false);
+            return;
+        } else if (props.variant.name === 'skybox') {
+            this.clearImage();
+            this.updateSkybox(camera, props.variant.params, onload);
+        } else if (props.variant.name === 'image') {
+            this.clearSkybox();
+            this.updateImage(props.variant.params, onload);
+        } else if (props.variant.name === 'horizontalGradient') {
+            this.clearSkybox();
+            this.clearImage();
+            this.updateGradient(props.variant.params.topColor, props.variant.params.bottomColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false);
+            onload?.(false);
+        } else if (props.variant.name === 'radialGradient') {
+            this.clearSkybox();
+            this.clearImage();
+            this.updateGradient(props.variant.params.centerColor, props.variant.params.edgeColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false);
+            onload?.(false);
+        }
+
+        const { x, y, width, height } = camera.viewport;
+        ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height));
+    }
+
+    isEnabled(props: BackgroundProps) {
+        return !!(
+            (this.skybox && this.skybox.loaded) ||
+            (this.image && this.image.loaded) ||
+            props.variant.name === 'horizontalGradient' ||
+            props.variant.name === 'radialGradient'
+        );
+    }
+
+    private isReady() {
+        return !!(
+            (this.skybox && this.skybox.loaded) ||
+            (this.image && this.image.loaded) ||
+            this.renderable.values.dVariant.ref.value === 'horizontalGradient' ||
+            this.renderable.values.dVariant.ref.value === 'radialGradient'
+        );
+    }
+
+    render() {
+        if (!this.isReady()) return;
+
+        if (this.renderable.values.dVariant.ref.value === 'image') {
+            this.updateImageScaling();
+        }
+
+        if (isTimingMode) this.webgl.timer.mark('BackgroundPass.render');
+        this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('BackgroundPass.render');
+    }
+
+    dispose() {
+        this.clearSkybox();
+        this.clearImage();
+    }
+}
+
+//
+
+const SkyboxName = 'background-skybox';
+
+type CubeAssets = { [k in keyof CubeFaces]: Asset };
+
+function getCubeAssets(assetManager: AssetManager, faces: SkyboxProps['faces']): CubeAssets {
+    if (faces.name === 'urls') {
+        return {
+            nx: Asset.getUrlAsset(assetManager, faces.params.nx),
+            ny: Asset.getUrlAsset(assetManager, faces.params.ny),
+            nz: Asset.getUrlAsset(assetManager, faces.params.nz),
+            px: Asset.getUrlAsset(assetManager, faces.params.px),
+            py: Asset.getUrlAsset(assetManager, faces.params.py),
+            pz: Asset.getUrlAsset(assetManager, faces.params.pz),
+        };
+    } else {
+        return {
+            nx: faces.params.nx!,
+            ny: faces.params.ny!,
+            nz: faces.params.nz!,
+            px: faces.params.px!,
+            py: faces.params.py!,
+            pz: faces.params.pz!,
+        };
+    }
+}
+
+function getCubeFaces(assetManager: AssetManager, cubeAssets: CubeAssets): CubeFaces {
+    const resolve = (asset: Asset) => {
+        return assetManager.resolve(asset, 'binary').run().then(a => new Blob([a.data]));
+    };
+
+    return {
+        nx: resolve(cubeAssets.nx),
+        ny: resolve(cubeAssets.ny),
+        nz: resolve(cubeAssets.nz),
+        px: resolve(cubeAssets.px),
+        py: resolve(cubeAssets.py),
+        pz: resolve(cubeAssets.pz),
+    };
+}
+
+function getSkyboxHash(faces: SkyboxProps['faces']) {
+    if (faces.name === 'urls') {
+        return `${SkyboxName}_${faces.params.nx}|${faces.params.ny}|${faces.params.nz}|${faces.params.px}|${faces.params.py}|${faces.params.pz}`;
+    } else {
+        return `${SkyboxName}_${faces.params.nx?.id}|${faces.params.ny?.id}|${faces.params.nz?.id}|${faces.params.px?.id}|${faces.params.py?.id}|${faces.params.pz?.id}`;
+    }
+}
+
+function areSkyboxTexturePropsEqual(facesA: SkyboxProps['faces'], facesB: SkyboxProps['faces']) {
+    return getSkyboxHash(facesA) === getSkyboxHash(facesB);
+}
+
+function getSkyboxTexture(ctx: WebGLContext, assetManager: AssetManager, faces: SkyboxProps['faces'], onload?: (errored?: boolean) => void): { texture: Texture, assets: Asset[] } {
+    const cubeAssets = getCubeAssets(assetManager, faces);
+    const cubeFaces = getCubeFaces(assetManager, cubeAssets);
+    const assets = [cubeAssets.nx, cubeAssets.ny, cubeAssets.nz, cubeAssets.px, cubeAssets.py, cubeAssets.pz];
+    const texture = ctx.resources.cubeTexture(cubeFaces, false, onload);
+    return { texture, assets };
+}
+
+//
+
+const ImageName = 'background-image';
+
+function getImageHash(source: ImageProps['source']) {
+    if (source.name === 'url') {
+        return `${ImageName}_${source.params}`;
+    } else {
+        return `${ImageName}_${source.params?.id}`;
+    }
+}
+
+function areImageTexturePropsEqual(sourceA: ImageProps['source'], sourceB: ImageProps['source']) {
+    return getImageHash(sourceA) === getImageHash(sourceB);
+}
+
+function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source: ImageProps['source'], onload?: (errored?: boolean) => void): { texture: Texture, asset: Asset } {
+    const texture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    const img = new Image();
+    img.onload = () => {
+        texture.load(img);
+        onload?.();
+    };
+    img.onerror = () => {
+        onload?.(true);
+    };
+    const asset = source.name === 'url'
+        ? Asset.getUrlAsset(assetManager, source.params)
+        : source.params!;
+    assetManager.resolve(asset, 'binary').run().then(a => {
+        const blob = new Blob([a.data]);
+        img.src = URL.createObjectURL(blob);
+    });
+    return { texture, asset };
+}
+
+//
+
+const BackgroundSchema = {
+    drawCount: ValueSpec('number'),
+    instanceCount: ValueSpec('number'),
+    aPosition: AttributeSpec('float32', 2, 0),
+    tSkybox: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    tImage: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uImageScale: UniformSpec('v2'),
+    uImageOffset: UniformSpec('v2'),
+    uTexSize: UniformSpec('v2'),
+    uViewport: UniformSpec('v4'),
+    uViewportAdjusted: UniformSpec('b'),
+    uViewDirectionProjectionInverse: UniformSpec('m4'),
+    uGradientColorA: UniformSpec('v3'),
+    uGradientColorB: UniformSpec('v3'),
+    uGradientRatio: UniformSpec('f'),
+    uOpacity: UniformSpec('f'),
+    uSaturation: UniformSpec('f'),
+    uLightness: UniformSpec('f'),
+    dVariant: DefineSpec('string', ['skybox', 'image', 'verticalGradient', 'horizontalGradient', 'radialGradient']),
+};
+const SkyboxShaderCode = ShaderCode('background', background_vert, background_frag);
+type BackgroundRenderable = ComputeRenderable<Values<typeof BackgroundSchema>>
+
+function getBackgroundRenderable(ctx: WebGLContext, width: number, height: number): BackgroundRenderable {
+    const values: Values<typeof BackgroundSchema> = {
+        drawCount: ValueCell.create(6),
+        instanceCount: ValueCell.create(1),
+        aPosition: ValueCell.create(QuadPositions),
+        tSkybox: ValueCell.create(createNullTexture()),
+        tImage: ValueCell.create(createNullTexture()),
+        uImageScale: ValueCell.create(Vec2()),
+        uImageOffset: ValueCell.create(Vec2()),
+        uTexSize: ValueCell.create(Vec2.create(width, height)),
+        uViewport: ValueCell.create(Vec4()),
+        uViewportAdjusted: ValueCell.create(true),
+        uViewDirectionProjectionInverse: ValueCell.create(Mat4()),
+        uGradientColorA: ValueCell.create(Vec3()),
+        uGradientColorB: ValueCell.create(Vec3()),
+        uGradientRatio: ValueCell.create(0.5),
+        uOpacity: ValueCell.create(1),
+        uSaturation: ValueCell.create(0),
+        uLightness: ValueCell.create(0),
+        dVariant: ValueCell.create('skybox'),
+    };
+
+    const schema = { ...BackgroundSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', SkyboxShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}

+ 49 - 45
src/mol-canvas3d/passes/draw.ts

@@ -21,10 +21,11 @@ import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './pos
 import { MarkingPass, MarkingProps } from './marking';
 import { MarkingPass, MarkingProps } from './marking';
 import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
 import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
 import { isTimingMode } from '../../mol-util/debug';
 import { isTimingMode } from '../../mol-util/debug';
+import { AssetManager } from '../../mol-util/assets';
 
 
 type Props = {
 type Props = {
-    postprocessing: PostprocessingProps
-    marking: MarkingProps
+    postprocessing: PostprocessingProps;
+    marking: MarkingProps;
     transparentBackground: boolean;
     transparentBackground: boolean;
 }
 }
 
 
@@ -50,7 +51,7 @@ export class DrawPass {
     private copyFboTarget: CopyRenderable;
     private copyFboTarget: CopyRenderable;
     private copyFboPostprocessing: CopyRenderable;
     private copyFboPostprocessing: CopyRenderable;
 
 
-    private wboit: WboitPass | undefined;
+    private readonly wboit: WboitPass | undefined;
     private readonly marking: MarkingPass;
     private readonly marking: MarkingPass;
     readonly postprocessing: PostprocessingPass;
     readonly postprocessing: PostprocessingPass;
     private readonly antialiasing: AntialiasingPass;
     private readonly antialiasing: AntialiasingPass;
@@ -59,11 +60,10 @@ export class DrawPass {
         return !!this.wboit?.supported;
         return !!this.wboit?.supported;
     }
     }
 
 
-    constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) {
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, width: number, height: number, enableWboit: boolean) {
         const { extensions, resources, isWebGL2 } = webgl;
         const { extensions, resources, isWebGL2 } = webgl;
 
 
         this.drawTarget = createNullRenderTarget(webgl.gl);
         this.drawTarget = createNullRenderTarget(webgl.gl);
-
         this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
         this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
         this.packedDepth = !extensions.depthTexture;
         this.packedDepth = !extensions.depthTexture;
 
 
@@ -79,7 +79,7 @@ export class DrawPass {
 
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
         this.marking = new MarkingPass(webgl, width, height);
         this.marking = new MarkingPass(webgl, width, height);
-        this.postprocessing = new PostprocessingPass(webgl, this);
+        this.postprocessing = new PostprocessingPass(webgl, assetManager, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
 
 
         this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
         this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
@@ -120,14 +120,13 @@ export class DrawPass {
     private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
     private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
         if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
 
 
-        this.colorTarget.bind();
+        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
         renderer.clear(true);
         renderer.clear(true);
 
 
         // render opaque primitives
         // render opaque primitives
-        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
-        this.colorTarget.bind();
-        renderer.clearDepth();
-        renderer.renderWboitOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderWboitOpaque(scene.primitives, camera, null);
+        }
 
 
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
             if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
             if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
@@ -165,14 +164,17 @@ export class DrawPass {
         if (toDrawingBuffer) {
         if (toDrawingBuffer) {
             this.drawTarget.bind();
             this.drawTarget.bind();
         } else {
         } else {
-            this.colorTarget.bind();
             if (!this.packedDepth) {
             if (!this.packedDepth) {
                 this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
                 this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            } else {
+                this.colorTarget.bind();
             }
             }
         }
         }
 
 
         renderer.clear(true);
         renderer.clear(true);
-        renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        }
 
 
         if (!toDrawingBuffer) {
         if (!toDrawingBuffer) {
             // do a depth pass if not rendering to drawing buffer and
             // do a depth pass if not rendering to drawing buffer and
@@ -235,7 +237,7 @@ export class DrawPass {
         }
         }
     }
     }
 
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, props: Props) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
         const volumeRendering = scene.volumes.renderables.length > 0;
         const volumeRendering = scene.volumes.renderables.length > 0;
         const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
         const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
         const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
         const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
@@ -245,54 +247,52 @@ export class DrawPass {
         renderer.setViewport(x, y, width, height);
         renderer.setViewport(x, y, width, height);
         renderer.update(camera);
         renderer.update(camera);
 
 
-        if (props.transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
+        if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
             this.drawTarget.bind();
             this.drawTarget.bind();
             renderer.clear(false);
             renderer.clear(false);
         }
         }
 
 
         if (this.wboitEnabled) {
         if (this.wboitEnabled) {
-            this._renderWboit(renderer, camera, scene, props.transparentBackground, props.postprocessing);
-        } else {
-            this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, props.transparentBackground, props.postprocessing);
-        }
-
-        if (postprocessingEnabled) {
-            this.postprocessing.target.bind();
-        } else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) {
-            this.colorTarget.bind();
+            this._renderWboit(renderer, camera, scene, transparentBackground, props.postprocessing);
         } else {
         } else {
-            this.drawTarget.bind();
+            this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, transparentBackground, props.postprocessing);
         }
         }
 
 
-        if (markingEnabled) {
-            if (scene.markerAverage > 0) {
-                const markingDepthTest = props.marking.ghostEdgeStrength < 1;
-                if (markingDepthTest && scene.markerAverage !== 1) {
-                    this.marking.depthTarget.bind();
-                    renderer.clear(false, true);
-                    renderer.renderMarkingDepth(scene.primitives, camera, null);
-                }
+        const target = postprocessingEnabled
+            ? this.postprocessing.target
+            : !toDrawingBuffer || volumeRendering || this.wboitEnabled
+                ? this.colorTarget
+                : this.drawTarget;
 
 
-                this.marking.maskTarget.bind();
+        if (markingEnabled && scene.markerAverage > 0) {
+            const markingDepthTest = props.marking.ghostEdgeStrength < 1;
+            if (markingDepthTest && scene.markerAverage !== 1) {
+                this.marking.depthTarget.bind();
                 renderer.clear(false, true);
                 renderer.clear(false, true);
-                renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
-
-                this.marking.update(props.marking);
-                this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+                renderer.renderMarkingDepth(scene.primitives, camera, null);
             }
             }
+
+            this.marking.maskTarget.bind();
+            renderer.clear(false, true);
+            renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
+
+            this.marking.update(props.marking);
+            this.marking.render(camera.viewport, target);
+        } else {
+            target.bind();
         }
         }
 
 
         if (helper.debug.isEnabled) {
         if (helper.debug.isEnabled) {
             helper.debug.syncVisibility();
             helper.debug.syncVisibility();
-            renderer.renderBlended(helper.debug.scene, camera, null);
+            renderer.renderBlended(helper.debug.scene, camera);
         }
         }
         if (helper.handle.isEnabled) {
         if (helper.handle.isEnabled) {
-            renderer.renderBlended(helper.handle.scene, camera, null);
+            renderer.renderBlended(helper.handle.scene, camera);
         }
         }
         if (helper.camera.isEnabled) {
         if (helper.camera.isEnabled) {
             helper.camera.update(camera);
             helper.camera.update(camera);
             renderer.update(helper.camera.camera);
             renderer.update(helper.camera.camera);
-            renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
+            renderer.renderBlended(helper.camera.scene, helper.camera.camera);
         }
         }
 
 
         if (antialiasingEnabled) {
         if (antialiasingEnabled) {
@@ -314,15 +314,19 @@ export class DrawPass {
     render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
     render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
         if (isTimingMode) this.webgl.timer.mark('DrawPass.render');
         if (isTimingMode) this.webgl.timer.mark('DrawPass.render');
         const { renderer, camera, scene, helper } = ctx;
         const { renderer, camera, scene, helper } = ctx;
-        renderer.setTransparentBackground(props.transparentBackground);
+
+        this.postprocessing.setTransparentBackground(props.transparentBackground);
+        const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing.background);
+
+        renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
         renderer.setPixelRatio(this.webgl.pixelRatio);
         renderer.setPixelRatio(this.webgl.pixelRatio);
 
 
         if (StereoCamera.is(camera)) {
         if (StereoCamera.is(camera)) {
-            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, props);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, props);
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, props);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, props);
         } else {
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer, props);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
         }
         }
         if (isTimingMode) this.webgl.timer.markEnd('DrawPass.render');
         if (isTimingMode) this.webgl.timer.markEnd('DrawPass.render');
     }
     }

+ 2 - 2
src/mol-canvas3d/passes/fxaa.ts

@@ -44,8 +44,8 @@ export class FxaaPass {
         state.depthMask(false);
         state.depthMask(false);
 
 
         const { x, y, width, height } = viewport;
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
 
         state.clearColor(0, 0, 0, 1);
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);
         gl.clear(gl.COLOR_BUFFER_BIT);

+ 12 - 3
src/mol-canvas3d/passes/image.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -18,6 +18,7 @@ import { PixelData } from '../../mol-util/image';
 import { Helper } from '../helper/helper';
 import { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { MarkingParams } from './marking';
 import { MarkingParams } from './marking';
+import { AssetManager } from '../../mol-util/assets';
 
 
 export const ImageParams = {
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
     transparentBackground: PD.Boolean(false),
@@ -47,10 +48,10 @@ export class ImagePass {
     get width() { return this._width; }
     get width() { return this._width; }
     get height() { return this._height; }
     get height() { return this._height; }
 
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) {
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) {
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
 
-        this.drawPass = new DrawPass(webgl, 128, 128, enableWboit);
+        this.drawPass = new DrawPass(webgl, assetManager, 128, 128, enableWboit);
         this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
         this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
 
 
@@ -63,6 +64,14 @@ export class ImagePass {
         this.setSize(1024, 768);
         this.setSize(1024, 768);
     }
     }
 
 
+    updateBackground() {
+        return new Promise<void>(resolve => {
+            this.drawPass.postprocessing.background.update(this.camera, this.props.postprocessing.background, () => {
+                resolve();
+            });
+        });
+    }
+
     setSize(width: number, height: number) {
     setSize(width: number, height: number) {
         if (width === this._width && height === this._height) return;
         if (width === this._width && height === this._height) return;
 
 

+ 4 - 4
src/mol-canvas3d/passes/marking.ts

@@ -64,8 +64,8 @@ export class MarkingPass {
         state.depthMask(false);
         state.depthMask(false);
 
 
         const { x, y, width, height } = viewport;
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
 
         state.clearColor(0, 0, 0, 0);
         state.clearColor(0, 0, 0, 0);
         gl.clear(gl.COLOR_BUFFER_BIT);
         gl.clear(gl.COLOR_BUFFER_BIT);
@@ -82,8 +82,8 @@ export class MarkingPass {
         state.depthMask(false);
         state.depthMask(false);
 
 
         const { x, y, width, height } = viewport;
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
     }
 
 
     setSize(width: number, height: number) {
     setSize(width: number, height: number) {

+ 10 - 10
src/mol-canvas3d/passes/multi-sample.ts

@@ -176,8 +176,8 @@ export class MultiSamplePass {
             state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
             state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
             state.disable(gl.DEPTH_TEST);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             if (i === 0) {
             if (i === 0) {
                 state.clearColor(0, 0, 0, 0);
                 state.clearColor(0, 0, 0, 0);
                 gl.clear(gl.COLOR_BUFFER_BIT);
                 gl.clear(gl.COLOR_BUFFER_BIT);
@@ -192,8 +192,8 @@ export class MultiSamplePass {
         compose.update();
         compose.update();
 
 
         this.bindOutputTarget(toDrawingBuffer);
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
 
         state.disable(gl.BLEND);
         state.disable(gl.BLEND);
         compose.render();
         compose.render();
@@ -231,8 +231,8 @@ export class MultiSamplePass {
             state.disable(gl.BLEND);
             state.disable(gl.BLEND);
             state.disable(gl.DEPTH_TEST);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             compose.render();
             compose.render();
             sampleIndex += 1;
             sampleIndex += 1;
         } else {
         } else {
@@ -267,8 +267,8 @@ export class MultiSamplePass {
                 state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
                 state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
                 state.disable(gl.DEPTH_TEST);
                 state.disable(gl.DEPTH_TEST);
                 state.depthMask(false);
                 state.depthMask(false);
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (sampleIndex === 0) {
                 if (sampleIndex === 0) {
                     state.clearColor(0, 0, 0, 0);
                     state.clearColor(0, 0, 0, 0);
                     gl.clear(gl.COLOR_BUFFER_BIT);
                     gl.clear(gl.COLOR_BUFFER_BIT);
@@ -283,8 +283,8 @@ export class MultiSamplePass {
         drawPass.postprocessing.setOcclusionOffset(0, 0);
         drawPass.postprocessing.setOcclusionOffset(0, 0);
 
 
         this.bindOutputTarget(toDrawingBuffer);
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
 
         const accumulationWeight = sampleIndex * sampleWeight;
         const accumulationWeight = sampleIndex * sampleWeight;
         if (accumulationWeight > 0) {
         if (accumulationWeight > 0) {

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

@@ -8,15 +8,16 @@ import { DrawPass } from './draw';
 import { PickPass } from './pick';
 import { PickPass } from './pick';
 import { MultiSamplePass } from './multi-sample';
 import { MultiSamplePass } from './multi-sample';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { AssetManager } from '../../mol-util/assets';
 
 
 export class Passes {
 export class Passes {
     readonly draw: DrawPass;
     readonly draw: DrawPass;
     readonly pick: PickPass;
     readonly pick: PickPass;
     readonly multiSample: MultiSamplePass;
     readonly multiSample: MultiSamplePass;
 
 
-    constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
         const { gl } = webgl;
         const { gl } = webgl;
-        this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
+        this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
         this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
         this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
         this.multiSample = new MultiSamplePass(webgl, this.draw);
         this.multiSample = new MultiSamplePass(webgl, this.draw);
     }
     }

+ 54 - 24
src/mol-canvas3d/passes/postprocessing.ts

@@ -28,6 +28,8 @@ import { Color } from '../../mol-util/color';
 import { FxaaParams, FxaaPass } from './fxaa';
 import { FxaaParams, FxaaPass } from './fxaa';
 import { SmaaParams, SmaaPass } from './smaa';
 import { SmaaParams, SmaaPass } from './smaa';
 import { isTimingMode } from '../../mol-util/debug';
 import { isTimingMode } from '../../mol-util/debug';
+import { BackgroundParams, BackgroundPass } from './background';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const OutlinesSchema = {
 const OutlinesSchema = {
     ...QuadSchema,
     ...QuadSchema,
@@ -91,7 +93,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
         ...QuadValues,
         ...QuadValues,
         tDepth: ValueCell.create(depthTexture),
         tDepth: ValueCell.create(depthTexture),
 
 
-        uSamples: ValueCell.create([0.0, 0.0, 1.0]),
+        uSamples: ValueCell.create(getSamples(32)),
         dNSamples: ValueCell.create(32),
         dNSamples: ValueCell.create(32),
 
 
         uProjection: ValueCell.create(Mat4.identity()),
         uProjection: ValueCell.create(Mat4.identity()),
@@ -138,7 +140,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
         tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
         uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
 
 
-        uKernel: ValueCell.create([0.0]),
+        uKernel: ValueCell.create(getBlurKernel(15)),
         dOcclusionKernelSize: ValueCell.create(15),
         dOcclusionKernelSize: ValueCell.create(15),
 
 
         uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
         uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
@@ -171,15 +173,26 @@ function getBlurKernel(kernelSize: number): number[] {
     return kernel;
     return kernel;
 }
 }
 
 
-function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
+const RandomHemisphereVector: Vec3[] = [];
+for (let i = 0; i < 256; i++) {
+    const v = Vec3();
+    v[0] = Math.random() * 2.0 - 1.0;
+    v[1] = Math.random() * 2.0 - 1.0;
+    v[2] = Math.random();
+    Vec3.normalize(v, v);
+    Vec3.scale(v, v, Math.random());
+    RandomHemisphereVector.push(v);
+}
+
+function getSamples(nSamples: number): number[] {
     const samples = [];
     const samples = [];
     for (let i = 0; i < nSamples; i++) {
     for (let i = 0; i < nSamples; i++) {
         let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
         let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
         scale = 0.1 + scale * (1.0 - 0.1);
         scale = 0.1 + scale * (1.0 - 0.1);
 
 
-        samples.push(vectorSamples[i][0] * scale);
-        samples.push(vectorSamples[i][1] * scale);
-        samples.push(vectorSamples[i][2] * scale);
+        samples.push(RandomHemisphereVector[i][0] * scale);
+        samples.push(RandomHemisphereVector[i][1] * scale);
+        samples.push(RandomHemisphereVector[i][2] * scale);
     }
     }
 
 
     return samples;
     return samples;
@@ -274,12 +287,13 @@ export const PostprocessingParams = {
         smaa: PD.Group(SmaaParams),
         smaa: PD.Group(SmaaParams),
         off: PD.Group({})
         off: PD.Group({})
     }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
     }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
+    background: PD.Group(BackgroundParams, { isFlat: true }),
 };
 };
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 
 export class PostprocessingPass {
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
     static isEnabled(props: PostprocessingProps) {
-        return props.occlusion.name === 'on' || props.outline.name === 'on';
+        return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
     }
     }
 
 
     static isOutlineEnabled(props: PostprocessingProps) {
     static isOutlineEnabled(props: PostprocessingProps) {
@@ -291,7 +305,6 @@ export class PostprocessingPass {
     private readonly outlinesTarget: RenderTarget;
     private readonly outlinesTarget: RenderTarget;
     private readonly outlinesRenderable: OutlinesRenderable;
     private readonly outlinesRenderable: OutlinesRenderable;
 
 
-    private readonly randomHemisphereVector: Vec3[];
     private readonly ssaoFramebuffer: Framebuffer;
     private readonly ssaoFramebuffer: Framebuffer;
     private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
     private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
     private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
     private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
@@ -318,7 +331,10 @@ export class PostprocessingPass {
         return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
         return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
     }
     }
 
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
+    private readonly bgColor = Vec3();
+    readonly background: BackgroundPass;
+
+    constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) {
         const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
         const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
         const width = colorTarget.getWidth();
         const width = colorTarget.getWidth();
         const height = colorTarget.getHeight();
         const height = colorTarget.getHeight();
@@ -334,16 +350,6 @@ export class PostprocessingPass {
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
 
 
-        this.randomHemisphereVector = [];
-        for (let i = 0; i < 256; i++) {
-            const v = Vec3();
-            v[0] = Math.random() * 2.0 - 1.0;
-            v[1] = Math.random() * 2.0 - 1.0;
-            v[2] = Math.random();
-            Vec3.normalize(v, v);
-            Vec3.scale(v, v, Math.random());
-            this.randomHemisphereVector.push(v);
-        }
         this.ssaoFramebuffer = webgl.resources.framebuffer();
         this.ssaoFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
@@ -368,6 +374,8 @@ export class PostprocessingPass {
         this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
         this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
         this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
         this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture);
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture);
+
+        this.background = new BackgroundPass(webgl, assetManager, width, height);
     }
     }
 
 
     setSize(width: number, height: number) {
     setSize(width: number, height: number) {
@@ -391,6 +399,8 @@ export class PostprocessingPass {
             ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
             ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
+
+            this.background.setSize(width, height);
         }
         }
     }
     }
 
 
@@ -440,7 +450,7 @@ export class PostprocessingPass {
                 needsUpdateSsao = true;
                 needsUpdateSsao = true;
 
 
                 this.nSamples = props.occlusion.params.samples;
                 this.nSamples = props.occlusion.params.samples;
-                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
             }
             }
             ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
             ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
@@ -538,8 +548,8 @@ export class PostprocessingPass {
         state.depthMask(false);
         state.depthMask(false);
 
 
         const { x, y, width, height } = camera.viewport;
         const { x, y, width, height } = camera.viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
     }
 
 
     private occlusionOffset: [x: number, y: number] = [0, 0];
     private occlusionOffset: [x: number, y: number] = [0, 0];
@@ -549,6 +559,11 @@ export class PostprocessingPass {
         ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y));
         ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y));
     }
     }
 
 
+    private transparentBackground = false;
+    setTransparentBackground(value: boolean) {
+        this.transparentBackground = value;
+    }
+
     render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
     render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
         if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
         if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
         this.updateState(camera, transparentBackground, backgroundColor, props);
         this.updateState(camera, transparentBackground, backgroundColor, props);
@@ -583,8 +598,23 @@ export class PostprocessingPass {
         }
         }
 
 
         const { gl, state } = this.webgl;
         const { gl, state } = this.webgl;
-        state.clearColor(0, 0, 0, 1);
-        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.background.update(camera, props.background);
+        if (this.background.isEnabled(props.background)) {
+            if (this.transparentBackground) {
+                state.clearColor(0, 0, 0, 0);
+            } else {
+                Color.toVec3Normalized(this.bgColor, backgroundColor);
+                state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1);
+            }
+            gl.clear(gl.COLOR_BUFFER_BIT);
+            state.enable(gl.BLEND);
+            state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+            this.background.render();
+        } else {
+            state.clearColor(0, 0, 0, 1);
+            gl.clear(gl.COLOR_BUFFER_BIT);
+        }
 
 
         this.renderable.render();
         this.renderable.render();
         if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');
         if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');

+ 2 - 2
src/mol-canvas3d/passes/smaa.ts

@@ -71,8 +71,8 @@ export class SmaaPass {
         state.depthMask(false);
         state.depthMask(false);
 
 
         const { x, y, width, height } = viewport;
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
 
         state.clearColor(0, 0, 0, 1);
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);
         gl.clear(gl.COLOR_BUFFER_BIT);

+ 11 - 1
src/mol-canvas3d/passes/wboit.ts

@@ -18,6 +18,8 @@ import { evaluateWboit_frag } from '../../mol-gl/shader/evaluate-wboit.frag';
 import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
 import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { isDebugMode, isTimingMode } from '../../mol-util/debug';
 import { isDebugMode, isTimingMode } from '../../mol-util/debug';
+import { isWebGL2 } from '../../mol-gl/webgl/compat';
+import { Renderbuffer } from '../../mol-gl/webgl/renderbuffer';
 
 
 const EvaluateWboitSchema = {
 const EvaluateWboitSchema = {
     ...QuadSchema,
     ...QuadSchema,
@@ -50,6 +52,7 @@ export class WboitPass {
     private readonly framebuffer: Framebuffer;
     private readonly framebuffer: Framebuffer;
     private readonly textureA: Texture;
     private readonly textureA: Texture;
     private readonly textureB: Texture;
     private readonly textureB: Texture;
+    private readonly depthRenderbuffer: Renderbuffer;
 
 
     private _supported = false;
     private _supported = false;
     get supported() {
     get supported() {
@@ -87,6 +90,7 @@ export class WboitPass {
         if (width !== w || height !== h) {
         if (width !== w || height !== h) {
             this.textureA.define(width, height);
             this.textureA.define(width, height);
             this.textureB.define(width, height);
             this.textureB.define(width, height);
+            this.depthRenderbuffer.setSize(width, height);
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
         }
         }
     }
     }
@@ -106,6 +110,8 @@ export class WboitPass {
 
 
         this.textureA.attachFramebuffer(this.framebuffer, 'color0');
         this.textureA.attachFramebuffer(this.framebuffer, 'color0');
         this.textureB.attachFramebuffer(this.framebuffer, 'color1');
         this.textureB.attachFramebuffer(this.framebuffer, 'color1');
+
+        this.depthRenderbuffer.attachFramebuffer(this.framebuffer);
     }
     }
 
 
     static isSupported(webgl: WebGLContext) {
     static isSupported(webgl: WebGLContext) {
@@ -128,7 +134,7 @@ export class WboitPass {
     constructor(private webgl: WebGLContext, width: number, height: number) {
     constructor(private webgl: WebGLContext, width: number, height: number) {
         if (!WboitPass.isSupported(webgl)) return;
         if (!WboitPass.isSupported(webgl)) return;
 
 
-        const { resources } = webgl;
+        const { resources, gl } = webgl;
 
 
         this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureA.define(width, height);
         this.textureA.define(width, height);
@@ -136,6 +142,10 @@ export class WboitPass {
         this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureB.define(width, height);
         this.textureB.define(width, height);
 
 
+        this.depthRenderbuffer = isWebGL2(gl)
+            ? resources.renderbuffer('depth32f', 'depth', width, height)
+            : resources.renderbuffer('depth16', 'depth', width, height);
+
         this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
         this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
         this.framebuffer = resources.framebuffer();
         this.framebuffer = resources.framebuffer();
 
 

+ 6 - 6
src/mol-geo/geometry/texture-mesh/color-smoothing.ts

@@ -319,8 +319,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
 
     if (isTimingMode) webgl.timer.mark('ColorAccumulate.render');
     if (isTimingMode) webgl.timer.mark('ColorAccumulate.render');
     setAccumulateDefaults(webgl);
     setAccumulateDefaults(webgl);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     gl.clear(gl.COLOR_BUFFER_BIT);
     ValueCell.update(uCurrentY, 0);
     ValueCell.update(uCurrentY, 0);
     let currCol = 0;
     let currCol = 0;
@@ -336,8 +336,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
         // console.log({ i, currX, currY });
         // console.log({ i, currX, currY });
         ValueCell.update(uCurrentX, currX);
         ValueCell.update(uCurrentX, currX);
         ValueCell.update(uCurrentSlice, i);
         ValueCell.update(uCurrentSlice, i);
-        gl.viewport(currX, currY, dx, dy);
-        gl.scissor(currX, currY, dx, dy);
+        state.viewport(currX, currY, dx, dy);
+        state.scissor(currX, currY, dx, dy);
         accumulateRenderable.render();
         accumulateRenderable.render();
         ++currCol;
         ++currCol;
         currX += dx;
         currX += dx;
@@ -371,8 +371,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
 
     setNormalizeDefaults(webgl);
     setNormalizeDefaults(webgl);
     texture.attachFramebuffer(framebuffer, 0);
     texture.attachFramebuffer(framebuffer, 0);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     gl.clear(gl.COLOR_BUFFER_BIT);
     normalizeRenderable.render();
     normalizeRenderable.render();
     if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render');
     if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render');

+ 2 - 2
src/mol-gl/compute/grid3d.ts

@@ -225,8 +225,8 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
 
 
 function resetGl(webgl: WebGLContext, w: number) {
 function resetGl(webgl: WebGLContext, w: number) {
     const { gl, state } = webgl;
     const { gl, state } = webgl;
-    gl.viewport(0, 0, w, w);
-    gl.scissor(0, 0, w, w);
+    state.viewport(0, 0, w, w);
+    state.scissor(0, 0, w, w);
     state.disable(gl.SCISSOR_TEST);
     state.disable(gl.SCISSOR_TEST);
     state.disable(gl.BLEND);
     state.disable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);
     state.disable(gl.DEPTH_TEST);

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

@@ -122,7 +122,7 @@ export interface HistogramPyramid {
 
 
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
     if (isTimingMode) ctx.timer.mark('createHistogramPyramid');
     if (isTimingMode) ctx.timer.mark('createHistogramPyramid');
-    const { gl } = ctx;
+    const { gl, state } = ctx;
     const w = inputTexture.getWidth();
     const w = inputTexture.getWidth();
     const h = inputTexture.getHeight();
     const h = inputTexture.getHeight();
 
 
@@ -146,7 +146,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     const framebuffer = getFramebuffer('pyramid', ctx);
     const framebuffer = getFramebuffer('pyramid', ctx);
     pyramidTex.attachFramebuffer(framebuffer, 0);
     pyramidTex.attachFramebuffer(framebuffer, 0);
 
 
-    gl.viewport(0, 0, maxSizeX, maxSizeY);
+    state.viewport(0, 0, maxSizeX, maxSizeY);
     if (isWebGL2(gl)) {
     if (isWebGL2(gl)) {
         gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
         gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
     } else {
     } else {
@@ -157,7 +157,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i));
     for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i));
 
 
     const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture);
     const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture);
-    ctx.state.currentRenderItemId = -1;
+    state.currentRenderItemId = -1;
     setRenderingDefaults(ctx);
     setRenderingDefaults(ctx);
 
 
     let offset = 0;
     let offset = 0;
@@ -176,15 +176,15 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
             ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture);
             ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture);
             renderable.update();
             renderable.update();
         }
         }
-        ctx.state.currentRenderItemId = -1;
-        gl.viewport(0, 0, size, size);
-        gl.scissor(0, 0, size, size);
+        state.currentRenderItemId = -1;
+        state.viewport(0, 0, size, size);
+        state.scissor(0, 0, size, size);
         if (isWebGL2(gl)) {
         if (isWebGL2(gl)) {
             gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
             gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
         } else {
         } else {
             gl.clear(gl.COLOR_BUFFER_BIT);
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
         }
-        gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+        state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
         renderable.render();
 
 
         pyramidTex.bind(0);
         pyramidTex.bind(0);

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

@@ -68,7 +68,7 @@ const sumInts = new Int32Array(4);
 
 
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
     if (isTimingMode) ctx.timer.mark('getHistopyramidSum');
     if (isTimingMode) ctx.timer.mark('getHistopyramidSum');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
 
 
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture);
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture);
     ctx.state.currentRenderItemId = -1;
     ctx.state.currentRenderItemId = -1;
@@ -89,7 +89,7 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
 
 
     setRenderingDefaults(ctx);
     setRenderingDefaults(ctx);
 
 
-    gl.viewport(0, 0, 1, 1);
+    state.viewport(0, 0, 1, 1);
     renderable.render();
     renderable.render();
     gl.finish();
     gl.finish();
 
 

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

@@ -85,7 +85,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
 
 
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
     if (isTimingMode) ctx.timer.mark('calcActiveVoxels');
     if (isTimingMode) ctx.timer.mark('calcActiveVoxels');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
     const width = volumeData.getWidth();
     const width = volumeData.getWidth();
     const height = volumeData.getHeight();
     const height = volumeData.getHeight();
 
 
@@ -106,10 +106,10 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
 
 
     activeVoxelsTex.attachFramebuffer(framebuffer, 0);
     activeVoxelsTex.attachFramebuffer(framebuffer, 0);
     setRenderingDefaults(ctx);
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     gl.clear(gl.COLOR_BUFFER_BIT);
-    gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+    state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
     renderable.render();
     renderable.render();
 
 
     // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim);
     // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim);

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

@@ -127,7 +127,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
 
     if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers');
     if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers');
-    const { gl, resources, extensions } = ctx;
+    const { gl, state, resources, extensions } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const width = pyramidTex.getWidth();
     const width = pyramidTex.getWidth();
 
 
@@ -192,7 +192,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     ]);
     ]);
 
 
     setRenderingDefaults(ctx);
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
+    state.viewport(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     gl.clear(gl.COLOR_BUFFER_BIT);
     renderable.render();
     renderable.render();
 
 

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

@@ -125,8 +125,8 @@ export function readAlphaTexture(ctx: WebGLContext, texture: Texture) {
     state.clearColor(0, 0, 0, 0);
     state.clearColor(0, 0, 0, 0);
     state.blendFunc(gl.ONE, gl.ONE);
     state.blendFunc(gl.ONE, gl.ONE);
     state.blendEquation(gl.FUNC_ADD);
     state.blendEquation(gl.FUNC_ADD);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     gl.clear(gl.COLOR_BUFFER_BIT);
     copy.render();
     copy.render();
 
 

+ 13 - 9
src/mol-gl/renderer.ts

@@ -64,7 +64,7 @@ interface Renderer {
     renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
-    renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlended: (group: Scene, camera: ICamera) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
@@ -359,8 +359,8 @@ namespace Renderer {
             state.colorMask(true, true, true, true);
             state.colorMask(true, true, true, true);
 
 
             const { x, y, width, height } = viewport;
             const { x, y, width, height } = viewport;
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
 
 
             globalUniformsNeedUpdate = true;
             globalUniformsNeedUpdate = true;
             state.currentRenderItemId = -1;
             state.currentRenderItemId = -1;
@@ -475,9 +475,13 @@ namespace Renderer {
             if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
             if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
         };
         };
 
 
-        const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
-            renderBlendedOpaque(group, camera, depthTexture);
-            renderBlendedTransparent(group, camera, depthTexture);
+        const renderBlended = (scene: Scene, camera: ICamera) => {
+            if (scene.hasOpaque) {
+                renderBlendedOpaque(scene, camera, null);
+            }
+            if (scene.opacityAverage < 1) {
+                renderBlendedTransparent(scene, camera, null);
+            }
         };
         };
 
 
         const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
         const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
@@ -591,7 +595,7 @@ namespace Renderer {
                 // TODO: simplify, handle in renderable.state???
                 // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
-                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit', Flag.None);
                     renderObject(r, 'colorWboit', Flag.None);
                 }
                 }
             }
             }
@@ -714,8 +718,8 @@ namespace Renderer {
                 }
                 }
             },
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
             setViewport: (x: number, y: number, width: number, height: number) => {
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) {
                 if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) {
                     Viewport.set(viewport, x, y, width, height);
                     Viewport.set(viewport, x, y, width, height);
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height));
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height));

+ 30 - 2
src/mol-gl/scene.ts

@@ -80,8 +80,12 @@ interface Scene extends Object3D {
     has: (o: GraphicsRenderObject) => boolean
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
     clear: () => void
     forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
     forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
+    /** Marker average of primitive renderables */
     readonly markerAverage: number
     readonly markerAverage: number
+    /** Opacity average of primitive renderables */
     readonly opacityAverage: number
     readonly opacityAverage: number
+    /** Is `true` if any primitive renderable (possibly) has any opaque part */
+    readonly hasOpaque: boolean
 }
 }
 
 
 namespace Scene {
 namespace Scene {
@@ -103,6 +107,7 @@ namespace Scene {
 
 
         let markerAverage = 0;
         let markerAverage = 0;
         let opacityAverage = 0;
         let opacityAverage = 0;
+        let hasOpaque = false;
 
 
         const object3d = Object3D.create();
         const object3d = Object3D.create();
         const { view, position, direction, up } = object3d;
         const { view, position, direction, up } = object3d;
@@ -160,7 +165,9 @@ namespace Scene {
             }
             }
 
 
             renderables.sort(renderableSort);
             renderables.sort(renderableSort);
+            markerAverage = calculateMarkerAverage();
             opacityAverage = calculateOpacityAverage();
             opacityAverage = calculateOpacityAverage();
+            hasOpaque = calculateHasOpaque();
             return true;
             return true;
         }
         }
 
 
@@ -182,7 +189,10 @@ namespace Scene {
             const newVisibleHash = computeVisibleHash();
             const newVisibleHash = computeVisibleHash();
             if (newVisibleHash !== visibleHash) {
             if (newVisibleHash !== visibleHash) {
                 boundingSphereVisibleDirty = true;
                 boundingSphereVisibleDirty = true;
+                markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
+                visibleHash = newVisibleHash;
                 return true;
                 return true;
             } else {
             } else {
                 return false;
                 return false;
@@ -212,12 +222,27 @@ namespace Scene {
                 // uAlpha is updated in "render" so we need to recompute it here
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
                 const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
                 const xray = p.values.dXrayShaded?.ref.value ? 0.5 : 1;
                 const xray = p.values.dXrayShaded?.ref.value ? 0.5 : 1;
-                opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray;
+                const fuzzy = p.values.dPointStyle?.ref.value === 'fuzzy' ? 0.5 : 1;
+                const text = p.values.dGeometryType.ref.value === 'text' ? 0.5 : 1;
+                opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text;
                 count += 1;
                 count += 1;
             }
             }
             return count > 0 ? opacityAverage / count : 0;
             return count > 0 ? opacityAverage / count : 0;
         }
         }
 
 
+        function calculateHasOpaque() {
+            if (primitives.length === 0) return false;
+            for (let i = 0, il = primitives.length; i < il; ++i) {
+                const p = primitives[i];
+                if (!p.state.visible) continue;
+
+                if (p.state.opaque) return true;
+                if (p.state.alphaFactor === 1 && p.values.alpha.ref.value === 1 && p.values.transparencyAverage.ref.value !== 1) return true;
+                if (p.values.dTransparentBackfaces?.ref.value === 'opaque') return true;
+            }
+            return false;
+        }
+
         return {
         return {
             view, position, direction, up,
             view, position, direction, up,
 
 
@@ -245,6 +270,7 @@ namespace Scene {
                 }
                 }
                 markerAverage = calculateMarkerAverage();
                 markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
             },
             },
             add: (o: GraphicsRenderObject) => commitQueue.add(o),
             add: (o: GraphicsRenderObject) => commitQueue.add(o),
             remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
             remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
@@ -281,7 +307,6 @@ namespace Scene {
                 if (boundingSphereVisibleDirty) {
                 if (boundingSphereVisibleDirty) {
                     calculateBoundingSphere(renderables, boundingSphereVisible, true);
                     calculateBoundingSphere(renderables, boundingSphereVisible, true);
                     boundingSphereVisibleDirty = false;
                     boundingSphereVisibleDirty = false;
-                    visibleHash = computeVisibleHash();
                 }
                 }
                 return boundingSphereVisible;
                 return boundingSphereVisible;
             },
             },
@@ -291,6 +316,9 @@ namespace Scene {
             get opacityAverage() {
             get opacityAverage() {
                 return opacityAverage;
                 return opacityAverage;
             },
             },
+            get hasOpaque() {
+                return hasOpaque;
+            },
         };
         };
     }
     }
 }
 }

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

@@ -292,7 +292,9 @@ const glsl300VertPrefixCommon = `
 const glsl300FragPrefixCommon = `
 const glsl300FragPrefixCommon = `
 #define varying in
 #define varying in
 #define texture2D texture
 #define texture2D texture
+#define textureCube texture
 #define texture2DLodEXT textureLod
 #define texture2DLodEXT textureLod
+#define textureCubeLodEXT textureLod
 
 
 #define gl_FragColor out_FragData0
 #define gl_FragColor out_FragData0
 #define gl_FragDepthEXT gl_FragDepth
 #define gl_FragDepthEXT gl_FragDepth

+ 85 - 0
src/mol-gl/shader/background.frag.ts

@@ -0,0 +1,85 @@
+export const background_frag = `
+precision mediump float;
+precision mediump samplerCube;
+precision mediump sampler2D;
+
+#if defined(dVariant_skybox)
+    uniform samplerCube tSkybox;
+    uniform mat4 uViewDirectionProjectionInverse;
+    uniform float uOpacity;
+    uniform float uSaturation;
+    uniform float uLightness;
+#elif defined(dVariant_image)
+    uniform sampler2D tImage;
+    uniform vec2 uImageScale;
+    uniform vec2 uImageOffset;
+    uniform float uOpacity;
+    uniform float uSaturation;
+    uniform float uLightness;
+#elif defined(dVariant_horizontalGradient) || defined(dVariant_radialGradient)
+    uniform vec3 uGradientColorA;
+    uniform vec3 uGradientColorB;
+    uniform float uGradientRatio;
+#endif
+
+uniform vec2 uTexSize;
+uniform vec4 uViewport;
+uniform bool uViewportAdjusted;
+varying vec4 vPosition;
+
+// TODO: add as general pp option to remove banding?
+// Iestyn's RGB dither from http://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
+vec3 ScreenSpaceDither(vec2 vScreenPos) {
+    vec3 vDither = vec3(dot(vec2(171.0, 231.0), vScreenPos.xy));
+    vDither.rgb = fract(vDither.rgb / vec3(103.0, 71.0, 97.0));
+    return vDither.rgb / 255.0;
+}
+
+vec3 saturateColor(vec3 c, float amount) {
+    // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
+    const vec3 W = vec3(0.2125, 0.7154, 0.0721);
+    vec3 intensity = vec3(dot(c, W));
+    return mix(intensity, c, 1.0 + amount);
+}
+
+vec3 lightenColor(vec3 c, float amount) {
+    return c + amount;
+}
+
+void main() {
+    #if defined(dVariant_skybox)
+        vec4 t = uViewDirectionProjectionInverse * vPosition;
+        gl_FragColor = textureCube(tSkybox, normalize(t.xyz / t.w));
+        gl_FragColor.a = uOpacity;
+        gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
+    #elif defined(dVariant_image)
+        vec2 coords;
+        if (uViewportAdjusted) {
+            coords = ((gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uImageScale) + uImageOffset;
+        } else {
+            coords = (gl_FragCoord.xy / uImageScale) + uImageOffset;
+        }
+        gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
+        gl_FragColor.a = uOpacity;
+        gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
+    #elif defined(dVariant_horizontalGradient)
+        float d;
+        if (uViewportAdjusted) {
+            d = ((gl_FragCoord.y - uViewport.y) * (uTexSize.y / uViewport.w) / uTexSize.y) + 1.0 - (uGradientRatio * 2.0);
+        } else {
+            d = (gl_FragCoord.y / uTexSize.y) + 1.0 - (uGradientRatio * 2.0);
+        }
+        gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, clamp(d, 0.0, 1.0)), 1.0);
+        gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy);
+    #elif defined(dVariant_radialGradient)
+        float d;
+        if (uViewportAdjusted) {
+            d = distance(vec2(0.5), (gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uTexSize) + uGradientRatio - 0.5;
+        } else {
+            d = distance(vec2(0.5), gl_FragCoord.xy / uTexSize) + uGradientRatio - 0.5;
+        }
+        gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, 1.0 - clamp(d, 0.0, 1.0)), 1.0);
+        gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy);
+    #endif
+}
+`;

+ 12 - 0
src/mol-gl/shader/background.vert.ts

@@ -0,0 +1,12 @@
+export const background_vert = `
+precision mediump float;
+
+attribute vec2 aPosition;
+
+varying vec4 vPosition;
+
+void main() {
+    vPosition = vec4(aPosition, 1.0, 1.0);
+    gl_Position = vec4(aPosition, 1.0, 1.0);
+}
+`;

+ 0 - 1
src/mol-gl/shader/chunks/color-frag-params.glsl.ts

@@ -36,7 +36,6 @@ uniform float uBumpiness;
             varying vec4 vColor;
             varying vec4 vColor;
         #endif
         #endif
     #else
     #else
-        // avoid flat until EXT_provoking_vertex is supported
         #ifdef requiredDrawBuffers
         #ifdef requiredDrawBuffers
             flat in vec4 vObject;
             flat in vec4 vObject;
             flat in vec4 vInstance;
             flat in vec4 vInstance;

+ 0 - 1
src/mol-gl/shader/chunks/color-vert-params.glsl.ts

@@ -64,7 +64,6 @@ uniform float uBumpiness;
             varying vec4 vColor;
             varying vec4 vColor;
         #endif
         #endif
     #else
     #else
-        // avoid flat until EXT_provoking_vertex is supported
         #ifdef requiredDrawBuffers
         #ifdef requiredDrawBuffers
             flat out vec4 vObject;
             flat out vec4 vObject;
             flat out vec4 vInstance;
             flat out vec4 vInstance;

+ 0 - 2
src/mol-gl/shader/chunks/common-frag-params.glsl.ts

@@ -17,7 +17,6 @@ uniform int uMarkingType;
         #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
         #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
             varying float vClipping;
             varying float vClipping;
         #else
         #else
-            // avoid flat until EXT_provoking_vertex is supported
             flat in float vClipping;
             flat in float vClipping;
         #endif
         #endif
     #endif
     #endif
@@ -36,7 +35,6 @@ uniform int uMarkingType;
     #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
     #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
         varying float vMarker;
         varying float vMarker;
     #else
     #else
-        // avoid flat until EXT_provoking_vertex is supported
         flat in float vMarker;
         flat in float vMarker;
     #endif
     #endif
 #endif
 #endif

+ 3 - 3
src/mol-gl/shader/chunks/common-vert-params.glsl.ts

@@ -24,7 +24,6 @@ uniform int uPickType;
         #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
         #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
             varying float vClipping;
             varying float vClipping;
         #else
         #else
-            // avoid flat until EXT_provoking_vertex is supported
             flat out float vClipping;
             flat out float vClipping;
         #endif
         #endif
     #endif
     #endif
@@ -37,7 +36,6 @@ uniform int uPickType;
     #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
     #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
         varying float vMarker;
         varying float vMarker;
     #else
     #else
-        // avoid flat until EXT_provoking_vertex is supported
         flat out float vMarker;
         flat out float vMarker;
     #endif
     #endif
 #endif
 #endif
@@ -46,7 +44,9 @@ varying vec3 vModelPosition;
 varying vec3 vViewPosition;
 varying vec3 vViewPosition;
 
 
 #if defined(noNonInstancedActiveAttribs)
 #if defined(noNonInstancedActiveAttribs)
-    #define VertexID gl_VertexID
+    // int() is needed for some Safari versions
+    // see https://bugs.webkit.org/show_bug.cgi?id=244152
+    #define VertexID int(gl_VertexID)
 #else
 #else
     attribute float aVertex;
     attribute float aVertex;
     #define VertexID int(aVertex)
     #define VertexID int(aVertex)

+ 5 - 5
src/mol-gl/shader/cylinders.frag.ts

@@ -109,14 +109,14 @@ void main() {
 
 
     vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
     vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
     vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
     vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
-    gl_FragDepthEXT = calcDepth(vViewPosition);
+    float fragmentDepth = calcDepth(vViewPosition);
 
 
-    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
+    if (fragmentDepth < 0.0) discard;
+    if (fragmentDepth > 1.0) discard;
 
 
-    if (gl_FragDepthEXT < 0.0) discard;
-    if (gl_FragDepthEXT > 1.0) discard;
+    gl_FragDepthEXT = fragmentDepth;
 
 
-    float fragmentDepth = gl_FragDepthEXT;
+    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
     #include assign_material_color
     #include assign_material_color
 
 
     #if defined(dRenderVariant_pick)
     #if defined(dRenderVariant_pick)

+ 7 - 7
src/mol-gl/shader/spheres.frag.ts

@@ -70,17 +70,17 @@ void main(void){
     }
     }
 
 
     vec3 vViewPosition = cameraPos;
     vec3 vViewPosition = cameraPos;
-    gl_FragDepthEXT = calcDepth(vViewPosition);
-    if (!flag && gl_FragDepthEXT >= 0.0) {
-        gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
+    float fragmentDepth = calcDepth(vViewPosition);
+    if (!flag && fragmentDepth >= 0.0) {
+        fragmentDepth = 0.0 + (0.0000001 / vRadius);
     }
     }
 
 
-    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
+    if (fragmentDepth < 0.0) discard;
+    if (fragmentDepth > 1.0) discard;
 
 
-    if (gl_FragDepthEXT < 0.0) discard;
-    if (gl_FragDepthEXT > 1.0) discard;
+    gl_FragDepthEXT = fragmentDepth;
 
 
-    float fragmentDepth = gl_FragDepthEXT;
+    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
     #include assign_material_color
     #include assign_material_color
 
 
     #if defined(dRenderVariant_pick)
     #if defined(dRenderVariant_pick)

+ 6 - 5
src/mol-gl/webgl/context.ts

@@ -142,12 +142,12 @@ export function readPixels(gl: GLRenderingContext, x: number, y: number, width:
     if (isDebugMode) checkError(gl);
     if (isDebugMode) checkError(gl);
 }
 }
 
 
-function getDrawingBufferPixelData(gl: GLRenderingContext) {
+function getDrawingBufferPixelData(gl: GLRenderingContext, state: WebGLState) {
     const w = gl.drawingBufferWidth;
     const w = gl.drawingBufferWidth;
     const h = gl.drawingBufferHeight;
     const h = gl.drawingBufferHeight;
     const buffer = new Uint8Array(w * h * 4);
     const buffer = new Uint8Array(w * h * 4);
     unbindFramebuffer(gl);
     unbindFramebuffer(gl);
-    gl.viewport(0, 0, w, h);
+    state.viewport(0, 0, w, h);
     readPixels(gl, 0, 0, w, h, buffer);
     readPixels(gl, 0, 0, w, h, buffer);
     return PixelData.flipY(PixelData.create(buffer, w, h));
     return PixelData.flipY(PixelData.create(buffer, w, h));
 }
 }
@@ -164,6 +164,7 @@ function createStats() {
             renderbuffer: 0,
             renderbuffer: 0,
             shader: 0,
             shader: 0,
             texture: 0,
             texture: 0,
+            cubeTexture: 0,
             vertexArray: 0,
             vertexArray: 0,
         },
         },
 
 
@@ -345,15 +346,15 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         readPixelsAsync,
         readPixelsAsync,
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
         waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
         waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
-        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
+        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl, state),
         clear: (red: number, green: number, blue: number, alpha: number) => {
         clear: (red: number, green: number, blue: number, alpha: number) => {
             unbindFramebuffer(gl);
             unbindFramebuffer(gl);
             state.enable(gl.SCISSOR_TEST);
             state.enable(gl.SCISSOR_TEST);
             state.depthMask(true);
             state.depthMask(true);
             state.colorMask(true, true, true, true);
             state.colorMask(true, true, true, true);
             state.clearColor(red, green, blue, alpha);
             state.clearColor(red, green, blue, alpha);
-            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
-            gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         },
         },
 
 

+ 3 - 3
src/mol-gl/webgl/render-item.ts

@@ -150,8 +150,8 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
         vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
     }
     }
 
 
-    let drawCount = values.drawCount.ref.value;
-    let instanceCount = values.instanceCount.ref.value;
+    let drawCount: number = values.drawCount.ref.value;
+    let instanceCount: number = values.instanceCount.ref.value;
 
 
     stats.drawCount += drawCount;
     stats.drawCount += drawCount;
     stats.instanceCount += instanceCount;
     stats.instanceCount += instanceCount;
@@ -168,7 +168,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         getProgram: (variant: T) => programs[variant],
         getProgram: (variant: T) => programs[variant],
 
 
         render: (variant: T, sharedTexturesCount: number) => {
         render: (variant: T, sharedTexturesCount: number) => {
-            if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return;
+            if (drawCount === 0 || instanceCount === 0) return;
             const program = programs[variant];
             const program = programs[variant];
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
                 program.setUniforms(uniformValueEntries);
                 program.setUniforms(uniformValueEntries);

+ 10 - 2
src/mol-gl/webgl/resources.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -17,7 +17,7 @@ import { hashString, hashFnv32a } from '../../mol-data/util';
 import { DefineValues, ShaderCode } from '../shader-code';
 import { DefineValues, ShaderCode } from '../shader-code';
 import { RenderableSchema } from '../renderable/schema';
 import { RenderableSchema } from '../renderable/schema';
 import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer';
 import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer';
-import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture } from './texture';
+import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture, CubeFaces, createCubeTexture } from './texture';
 import { VertexArray, createVertexArray } from './vertex-array';
 import { VertexArray, createVertexArray } from './vertex-array';
 
 
 function defineValueHash(v: boolean | number | string): number {
 function defineValueHash(v: boolean | number | string): number {
@@ -59,6 +59,7 @@ export interface WebGLResources {
     renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
     renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
     shader: (type: ShaderType, source: string) => Shader
     shader: (type: ShaderType, source: string) => Shader
     texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
     texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
+    cubeTexture: (faces: CubeFaces, mipaps: boolean, onload?: () => void) => Texture,
     vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
     vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
 
 
     getByteCounts: () => ByteCounts
     getByteCounts: () => ByteCounts
@@ -76,6 +77,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
         renderbuffer: new Set<Resource>(),
         renderbuffer: new Set<Resource>(),
         shader: new Set<Resource>(),
         shader: new Set<Resource>(),
         texture: new Set<Resource>(),
         texture: new Set<Resource>(),
+        cubeTexture: new Set<Resource>(),
         vertexArray: new Set<Resource>(),
         vertexArray: new Set<Resource>(),
     };
     };
 
 
@@ -137,6 +139,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
         texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => {
         texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => {
             return wrap('texture', createTexture(gl, extensions, kind, format, type, filter));
             return wrap('texture', createTexture(gl, extensions, kind, format, type, filter));
         },
         },
+        cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => {
+            return wrap('cubeTexture', createCubeTexture(gl, faces, mipmaps, onload));
+        },
         vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
         vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
             return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer));
             return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer));
         },
         },
@@ -146,6 +151,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
             sets.texture.forEach(r => {
             sets.texture.forEach(r => {
                 texture += (r as Texture).getByteCount();
                 texture += (r as Texture).getByteCount();
             });
             });
+            sets.cubeTexture.forEach(r => {
+                texture += (r as Texture).getByteCount();
+            });
 
 
             let attribute = 0;
             let attribute = 0;
             sets.attribute.forEach(r => {
             sets.attribute.forEach(r => {

+ 29 - 0
src/mol-gl/webgl/state.ts

@@ -69,6 +69,9 @@ export type WebGLState = {
     clearVertexAttribsState: () => void
     clearVertexAttribsState: () => void
     disableUnusedVertexAttribs: () => void
     disableUnusedVertexAttribs: () => void
 
 
+    viewport: (x: number, y: number, width: number, height: number) => void
+    scissor: (x: number, y: number, width: number, height: number) => void
+
     reset: () => void
     reset: () => void
 }
 }
 
 
@@ -95,6 +98,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
     let maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
     let maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
     const vertexAttribsState: number[] = [];
     const vertexAttribsState: number[] = [];
 
 
+    let currentViewport: [number, number, number, number] = gl.getParameter(gl.VIEWPORT);
+    let currentScissor: [number, number, number, number] = gl.getParameter(gl.SCISSOR_BOX);
+
     const clearVertexAttribsState = () => {
     const clearVertexAttribsState = () => {
         for (let i = 0; i < maxVertexAttribs; ++i) {
         for (let i = 0; i < maxVertexAttribs; ++i) {
             vertexAttribsState[i] = 0;
             vertexAttribsState[i] = 0;
@@ -222,6 +228,26 @@ export function createState(gl: GLRenderingContext): WebGLState {
             }
             }
         },
         },
 
 
+        viewport: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentViewport[0] || y !== currentViewport[1] || width !== currentViewport[2] || height !== currentViewport[3]) {
+                gl.viewport(x, y, width, height);
+                currentViewport[0] = x;
+                currentViewport[1] = y;
+                currentViewport[2] = width;
+                currentViewport[3] = height;
+            }
+        },
+
+        scissor: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentScissor[0] || y !== currentScissor[1] || width !== currentScissor[2] || height !== currentScissor[3]) {
+                gl.scissor(x, y, width, height);
+                currentScissor[0] = x;
+                currentScissor[1] = y;
+                currentScissor[2] = width;
+                currentScissor[3] = height;
+            }
+        },
+
         reset: () => {
         reset: () => {
             enabledCapabilities = {};
             enabledCapabilities = {};
 
 
@@ -247,6 +273,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
             for (let i = 0; i < maxVertexAttribs; ++i) {
             for (let i = 0; i < maxVertexAttribs; ++i) {
                 vertexAttribsState[i] = 0;
                 vertexAttribsState[i] = 0;
             }
             }
+
+            currentViewport = gl.getParameter(gl.VIEWPORT);
+            currentScissor = gl.getParameter(gl.SCISSOR_BOX);
         }
         }
     };
     };
 }
 }

+ 119 - 1
src/mol-gl/webgl/texture.ts

@@ -11,8 +11,9 @@ import { RenderableSchema } from '../renderable/schema';
 import { idFactory } from '../../mol-util/id-factory';
 import { idFactory } from '../../mol-util/id-factory';
 import { Framebuffer } from './framebuffer';
 import { Framebuffer } from './framebuffer';
 import { isWebGL2, GLRenderingContext } from './compat';
 import { isWebGL2, GLRenderingContext } from './compat';
-import { ValueOf } from '../../mol-util/type-helpers';
+import { isPromiseLike, ValueOf } from '../../mol-util/type-helpers';
 import { WebGLExtensions } from './extensions';
 import { WebGLExtensions } from './extensions';
+import { objectForEach } from '../../mol-util/object';
 
 
 const getNextTextureId = idFactory();
 const getNextTextureId = idFactory();
 
 
@@ -423,6 +424,123 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture:
 
 
 //
 //
 
 
+export type CubeSide = 'nx' | 'ny' | 'nz' | 'px' | 'py' | 'pz';
+
+export type CubeFaces = {
+    [k in CubeSide]: string | File | Promise<Blob>;
+}
+
+export function getCubeTarget(gl: GLRenderingContext, side: CubeSide): number {
+    switch (side) {
+        case 'nx': return gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
+        case 'ny': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
+        case 'nz': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
+        case 'px': return gl.TEXTURE_CUBE_MAP_POSITIVE_X;
+        case 'py': return gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
+        case 'pz': return gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
+    }
+}
+
+export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipmaps: boolean, onload?: (errored?: boolean) => void): Texture {
+    const target = gl.TEXTURE_CUBE_MAP;
+    const filter = gl.LINEAR;
+    const internalFormat = gl.RGBA;
+    const format = gl.RGBA;
+    const type = gl.UNSIGNED_BYTE;
+
+    let size = 0;
+
+    const texture = gl.createTexture();
+    gl.bindTexture(target, texture);
+
+    let loadedCount = 0;
+    objectForEach(faces, (source, side) => {
+        if (!source) return;
+
+        const level = 0;
+        const cubeTarget = getCubeTarget(gl, side as CubeSide);
+
+        const image = new Image();
+        if (source instanceof File) {
+            image.src = URL.createObjectURL(source);
+        } else if (isPromiseLike(source)) {
+            source.then(blob => {
+                image.src = URL.createObjectURL(blob);
+            });
+        } else {
+            image.src = source;
+        }
+        image.addEventListener('load', () => {
+            if (size === 0) size = image.width;
+
+            gl.texImage2D(cubeTarget, level, internalFormat, size, size, 0, format, type, null);
+            gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
+            gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
+            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
+            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+            gl.bindTexture(target, texture);
+            gl.texImage2D(cubeTarget, level, internalFormat, format, type, image);
+
+            loadedCount += 1;
+            if (loadedCount === 6) {
+                if (!destroyed) {
+                    if (mipmaps) {
+                        gl.generateMipmap(target);
+                        gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
+                    } else {
+                        gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter);
+                    }
+                    gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter);
+                }
+                onload?.(destroyed);
+            }
+        });
+        image.addEventListener('error', () => {
+            onload?.(true);
+        });
+    });
+
+    let destroyed = false;
+
+    return {
+        id: getNextTextureId(),
+        target,
+        format,
+        internalFormat,
+        type,
+        filter,
+
+        getWidth: () => size,
+        getHeight: () => size,
+        getDepth: () => 0,
+        getByteCount: () => {
+            return getByteCount('rgba', 'ubyte', size, size, 0) * 6 * (mipmaps ? 2 : 1);
+        },
+
+        define: () => {},
+        load: () => {},
+        bind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, texture);
+        },
+        unbind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, null);
+        },
+        attachFramebuffer: () => {},
+        detachFramebuffer: () => {},
+
+        reset: () => {},
+        destroy: () => {
+            if (destroyed) return;
+            gl.deleteTexture(texture);
+            destroyed = true;
+        },
+    };
+}
+
+//
+
 export function createNullTexture(gl?: GLRenderingContext): Texture {
 export function createNullTexture(gl?: GLRenderingContext): Texture {
     const target = gl?.TEXTURE_2D ?? 3553;
     const target = gl?.TEXTURE_2D ?? 3553;
     return {
     return {

+ 1 - 1
src/mol-io/reader/cif/schema/bird.ts

@@ -1,7 +1,7 @@
 /**
 /**
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
- * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.360, IHM 1.17, MA 1.4.2.
  *
  *
  * @author molstar/ciftools package
  * @author molstar/ciftools package
  */
  */

+ 1 - 1
src/mol-io/reader/cif/schema/ccd.ts

@@ -1,7 +1,7 @@
 /**
 /**
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
- * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.360, IHM 1.17, MA 1.4.2.
  *
  *
  * @author molstar/ciftools package
  * @author molstar/ciftools package
  */
  */

+ 1 - 1
src/mol-io/reader/cif/schema/mmcif.ts

@@ -1,7 +1,7 @@
 /**
 /**
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
- * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.360, IHM 1.17, MA 1.4.2.
  *
  *
  * @author molstar/ciftools package
  * @author molstar/ciftools package
  */
  */

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

@@ -166,8 +166,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
         state.currentRenderItemId = -1;
         state.currentRenderItemId = -1;
         fbTex.attachFramebuffer(framebuffer, 0);
         fbTex.attachFramebuffer(framebuffer, 0);
         if (clear) {
         if (clear) {
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
+            state.viewport(0, 0, width, height);
+            state.scissor(0, 0, width, height);
             gl.clear(gl.COLOR_BUFFER_BIT);
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
         }
         ValueCell.update(uCurrentY, 0);
         ValueCell.update(uCurrentY, 0);
@@ -184,8 +184,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
             // console.log({ i, currX, currY });
             // console.log({ i, currX, currY });
             ValueCell.update(uCurrentX, currX);
             ValueCell.update(uCurrentX, currX);
             ValueCell.update(uCurrentSlice, i);
             ValueCell.update(uCurrentSlice, i);
-            gl.viewport(currX, currY, dx, dy);
-            gl.scissor(currX, currY, dx, dy);
+            state.viewport(currX, currY, dx, dy);
+            state.scissor(currX, currY, dx, dy);
             renderable.render();
             renderable.render();
             ++currCol;
             ++currCol;
             currX += dx;
             currX += dx;
@@ -232,8 +232,8 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     const framebuffer = getFramebuffer(webgl);
     const framebuffer = getFramebuffer(webgl);
     framebuffer.bind();
     framebuffer.bind();
     setRenderingDefaults(webgl);
     setRenderingDefaults(webgl);
-    gl.viewport(0, 0, dx, dy);
-    gl.scissor(0, 0, dx, dy);
+    state.viewport(0, 0, dx, dy);
+    state.scissor(0, 0, dx, dy);
 
 
     if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
     if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
         ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear')
         ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear')

+ 11 - 3
src/mol-math/geometry/primitives/box3d.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -124,11 +124,19 @@ namespace Box3D {
     }
     }
 
 
     export function containsVec3(box: Box3D, v: Vec3) {
     export function containsVec3(box: Box3D, v: Vec3) {
-        return (
+        return !(
             v[0] < box.min[0] || v[0] > box.max[0] ||
             v[0] < box.min[0] || v[0] > box.max[0] ||
             v[1] < box.min[1] || v[1] > box.max[1] ||
             v[1] < box.min[1] || v[1] > box.max[1] ||
             v[2] < box.min[2] || v[2] > box.max[2]
             v[2] < box.min[2] || v[2] > box.max[2]
-        ) ? false : true;
+        );
+    }
+
+    export function overlaps(a: Box3D, b: Box3D) {
+        return !(
+            a.max[0] < b.min[0] || a.min[0] > b.max[0] ||
+            a.max[1] < b.min[1] || a.min[1] > b.max[1] ||
+            a.max[2] < b.min[2] || a.min[2] > b.max[2]
+        );
     }
     }
 }
 }
 
 

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

@@ -35,7 +35,7 @@ function Vec3() {
 
 
 namespace Vec3 {
 namespace Vec3 {
     export function zero(): Vec3 {
     export function zero(): Vec3 {
-        const out = [0.1, 0.0, 0.0];
+        const out = [0.1, 0.0, 0.0]; // ensure backing array of type double
         out[0] = 0;
         out[0] = 0;
         return out as any;
         return out as any;
     }
     }

+ 4 - 1
src/mol-model-formats/structure/mol.ts

@@ -80,7 +80,10 @@ export async function getMolModels(mol: MolFile, format: ModelFormat<any> | unde
         const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
         const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
         const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdxB, x => x - 1, Int32Array));
         const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdxB, x => x - 1, Int32Array));
         const order = Column.asArrayColumn(bonds.order, Int32Array);
         const order = Column.asArrayColumn(bonds.order, Int32Array);
-        const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count });
+        const pairBonds = IndexPairBonds.fromData(
+            { pairs: { indexA, indexB, order }, count: atoms.count },
+            { maxDistance: Infinity }
+        );
         IndexPairBonds.Provider.set(models.representative, pairBonds);
         IndexPairBonds.Provider.set(models.representative, pairBonds);
     }
     }
 
 

+ 4 - 1
src/mol-model-formats/structure/mol2.ts

@@ -113,7 +113,10 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
                         return BondType.Flag.Covalent;
                         return BondType.Flag.Covalent;
                 }
                 }
             }, Int8Array));
             }, Int8Array));
-            const pairBonds = IndexPairBonds.fromData({ pairs: { key, indexA, indexB, order, flag }, count: atoms.count });
+            const pairBonds = IndexPairBonds.fromData(
+                { pairs: { key, indexA, indexB, order, flag }, count: atoms.count },
+                { maxDistance: crysin ? -1 : Infinity }
+            );
 
 
             const first = _models.representative;
             const first = _models.representative;
             IndexPairBonds.Provider.set(first, pairBonds);
             IndexPairBonds.Provider.set(first, pairBonds);

+ 1 - 1
src/mol-model-props/common/custom-element-property.ts

@@ -106,7 +106,7 @@ namespace CustomElementProperty {
             factory: Coloring,
             factory: Coloring,
             getParams: () => ({}),
             getParams: () => ({}),
             defaultValues: {},
             defaultValues: {},
-            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !!modelProperty.get(ctx.structure.models[0]).value,
+            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
             ensureCustomProperties: {
             ensureCustomProperties: {
                 attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? modelProperty.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
                 attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? modelProperty.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
                 detach: (data: ThemeDataContext) => data.structure && data.structure.models[0].customProperties.reference(modelProperty.descriptor, false)
                 detach: (data: ThemeDataContext) => data.structure && data.structure.models[0].customProperties.reference(modelProperty.descriptor, false)

+ 48 - 14
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -22,6 +22,8 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { InteractionFlag } from '../interactions/common';
 import { InteractionFlag } from '../interactions/common';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
@@ -31,7 +33,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
     const { contacts, unitsFeatures } = interactions;
     const { contacts, unitsFeatures } = interactions;
 
 
     const { edgeCount, edges } = contacts;
     const { edgeCount, edges } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
 
@@ -70,14 +72,48 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 
 
             if (child) {
             if (child) {
                 const b = edges[edgeIndex];
                 const b = edges[edgeIndex];
-                const childUnitA = child.unitMap.get(b.unitA);
-                if (!childUnitA) return true;
-
-                const unitA = structure.unitMap.get(b.unitA);
-                const { offsets, members } = unitsFeatures.get(b.unitA);
-                for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
-                    const eA = unitA.elements[members[i]];
-                    if (!SortedArray.has(childUnitA.elements, eA)) return true;
+
+                if (parentDisplay === 'stub') {
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) return true;
+
+                    const unitA = structure.unitMap.get(b.unitA);
+                    const { offsets, members } = unitsFeatures.get(b.unitA);
+                    for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                        const eA = unitA.elements[members[i]];
+                        if (!SortedArray.has(childUnitA.elements, eA)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) {
+                        flagA = true;
+                    } else {
+                        const unitA = structure.unitMap.get(b.unitA);
+                        const { offsets, members } = unitsFeatures.get(b.unitA);
+                        for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                            const eA = unitA.elements[members[i]];
+                            if (!SortedArray.has(childUnitA.elements, eA)) flagA = true;
+                        }
+                    }
+
+                    const childUnitB = child.unitMap.get(b.unitB);
+                    if (!childUnitB) {
+                        flagB = true;
+                    } else {
+                        const unitB = structure.unitMap.get(b.unitB);
+                        const { offsets, members } = unitsFeatures.get(b.unitB);
+                        for (let i = offsets[b.indexB], il = offsets[b.indexB + 1]; i < il; ++i) {
+                            const eB = unitB.elements[members[i]];
+                            if (!SortedArray.has(childUnitB.elements, eB)) flagB = true;
+                        }
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
                 }
             }
             }
 
 
@@ -101,10 +137,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 export const InteractionsInterUnitParams = {
 export const InteractionsInterUnitParams = {
     ...ComplexMeshParams,
     ...ComplexMeshParams,
     ...LinkCylinderParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 };
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 
 
@@ -121,7 +154,8 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
             );
 
 
             const interactionsHash = InteractionsProvider.get(newStructure).version;
             const interactionsHash = InteractionsProvider.get(newStructure).version;

+ 31 - 10
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -22,6 +22,8 @@ import { Interactions } from '../interactions/interactions';
 import { InteractionFlag } from '../interactions/common';
 import { InteractionFlag } from '../interactions/common';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
@@ -38,7 +40,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 
 
     const { x, y, z, members, offsets } = features;
     const { x, y, z, members, offsets } = features;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
 
@@ -60,10 +62,31 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
 
 
             if (childUnit) {
             if (childUnit) {
-                const f = a[edgeIndex];
-                for (let i = offsets[f], jl = offsets[f + 1]; i < jl; ++i) {
-                    const e = unit.elements[members[offsets[i]]];
-                    if (!SortedArray.has(childUnit.elements, e)) return true;
+                if (parentDisplay === 'stub') {
+                    const f = a[edgeIndex];
+                    for (let i = offsets[f], il = offsets[f + 1]; i < il; ++i) {
+                        const e = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, e)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const fA = a[edgeIndex];
+                    for (let i = offsets[fA], il = offsets[fA + 1]; i < il; ++i) {
+                        const eA = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eA)) flagA = true;
+                    }
+
+                    const fB = b[edgeIndex];
+                    for (let i = offsets[fB], il = offsets[fB + 1]; i < il; ++i) {
+                        const eB = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eB)) flagB = true;
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
                 }
             }
             }
 
 
@@ -86,10 +109,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 export const InteractionsIntraUnitParams = {
 export const InteractionsIntraUnitParams = {
     ...UnitsMeshParams,
     ...UnitsMeshParams,
     ...LinkCylinderParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 };
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 
 
@@ -106,7 +126,8 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
             );
 
 
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;

+ 16 - 0
src/mol-model-props/computed/representations/shared.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2022 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';
+
+export const InteractionsSharedParams = {
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
+    includeParent: PD.Boolean(false),
+    parentDisplay: PD.Select('stub', PD.arrayToOptions(['stub', 'full', 'between'] as const), { description: 'Only has an effect when "includeParent" is enabled. "Stub" shows just the child side of interactions to the parent. "Full" shows both sides of interactions to the parent. "Between" shows only interactions to the parent.' }),
+};
+export type InteractionsSharedParams = typeof InteractionsSharedParams

+ 0 - 10
src/mol-model/structure/structure/carbohydrates/constants.ts

@@ -386,16 +386,6 @@ const DefaultSaccharideCompIdMap = (function () {
                 map.set(charmm[j], saccharide);
                 map.set(charmm[j], saccharide);
             }
             }
         }
         }
-
-        const glycam = GlycamSaccharideNames[saccharide.abbr];
-        if (glycam) {
-            for (let j = 0, jl = glycam.length; j < jl; ++j) {
-                // On collision, use PDB name as default.
-                if (!map.has(glycam[j])) {
-                    map.set(glycam[j], saccharide);
-                }
-            }
-        }
     }
     }
     SaccharideNames.forEach(name => {
     SaccharideNames.forEach(name => {
         if (!map.has(name)) map.set(name, UnknownSaccharideComponent);
         if (!map.has(name)) map.set(name, UnknownSaccharideComponent);

+ 32 - 4
src/mol-model/structure/structure/structure.ts

@@ -23,7 +23,7 @@ import { Carbohydrates } from './carbohydrates/data';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { idFactory } from '../../../mol-util/id-factory';
 import { idFactory } from '../../../mol-util/id-factory';
-import { GridLookup3D } from '../../../mol-math/geometry';
+import { Box3D, GridLookup3D } from '../../../mol-math/geometry';
 import { UUID } from '../../../mol-util';
 import { UUID } from '../../../mol-util';
 import { CustomProperties } from '../../custom-property';
 import { CustomProperties } from '../../custom-property';
 import { AtomicHierarchy } from '../model/properties/atomic';
 import { AtomicHierarchy } from '../model/properties/atomic';
@@ -43,6 +43,8 @@ type State = {
     lookup3d?: StructureLookup3D,
     lookup3d?: StructureLookup3D,
     interUnitBonds?: InterUnitBonds,
     interUnitBonds?: InterUnitBonds,
     dynamicBonds: boolean,
     dynamicBonds: boolean,
+    interBondsValidUnit?: (unit: Unit) => boolean,
+    interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean,
     unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
     unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
     unitSymmetryGroupsIndexMap?: IntMap<number>,
     unitSymmetryGroupsIndexMap?: IntMap<number>,
     unitsSortedByVolume?: ReadonlyArray<Unit>;
     unitsSortedByVolume?: ReadonlyArray<Unit>;
@@ -241,6 +243,8 @@ class Structure {
             this.state.interUnitBonds = computeInterUnitBonds(this, {
             this.state.interUnitBonds = computeInterUnitBonds(this, {
                 ignoreWater: !this.dynamicBonds,
                 ignoreWater: !this.dynamicBonds,
                 ignoreIon: !this.dynamicBonds,
                 ignoreIon: !this.dynamicBonds,
+                validUnit: this.state.interBondsValidUnit,
+                validUnitPair: this.state.interBondsValidUnitPair,
             });
             });
         }
         }
         return this.state.interUnitBonds;
         return this.state.interUnitBonds;
@@ -250,6 +254,14 @@ class Structure {
         return this.state.dynamicBonds;
         return this.state.dynamicBonds;
     }
     }
 
 
+    get interBondsValidUnit() {
+        return this.state.interBondsValidUnit;
+    }
+
+    get interBondsValidUnitPair() {
+        return this.state.interBondsValidUnitPair;
+    }
+
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
         if (this.state.unitSymmetryGroups) return this.state.unitSymmetryGroups;
         if (this.state.unitSymmetryGroups) return this.state.unitSymmetryGroups;
         this.state.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
         this.state.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
@@ -380,7 +392,12 @@ class Structure {
             parent: parent?.remapModel(m),
             parent: parent?.remapModel(m),
             label: this.label,
             label: this.label,
             interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
             interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
-            dynamicBonds
+            dynamicBonds,
+            interBondsValidUnit: this.state.interBondsValidUnit,
+            interBondsValidUnitPair: this.state.interBondsValidUnitPair,
+            coordinateSystem: this.state.coordinateSystem,
+            masterModel: this.state.masterModel,
+            representativeModel: this.state.representativeModel,
         });
         });
     }
     }
 
 
@@ -428,7 +445,6 @@ class Structure {
 
 
 function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) {
 function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) {
     return units[i].id - units[j].id;
     return units[i].id - units[j].id;
-
 }
 }
 
 
 function getModels(s: Structure) {
 function getModels(s: Structure) {
@@ -634,6 +650,8 @@ namespace Structure {
          * Also enables calculation of inter-unit bonds in water molecules.
          * Also enables calculation of inter-unit bonds in water molecules.
          */
          */
         dynamicBonds?: boolean,
         dynamicBonds?: boolean,
+        interBondsValidUnit?: (unit: Unit) => boolean,
+        interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean,
         coordinateSystem?: SymmetryOperator
         coordinateSystem?: SymmetryOperator
         label?: string
         label?: string
         /** Master model for structures of a protein model and multiple ligand models */
         /** Master model for structures of a protein model and multiple ligand models */
@@ -722,6 +740,12 @@ namespace Structure {
         if (props.parent) state.parent = props.parent.parent || props.parent;
         if (props.parent) state.parent = props.parent.parent || props.parent;
         if (props.interUnitBonds) state.interUnitBonds = props.interUnitBonds;
         if (props.interUnitBonds) state.interUnitBonds = props.interUnitBonds;
 
 
+        if (props.interBondsValidUnit) state.interBondsValidUnit = props.interBondsValidUnit;
+        else if (props.parent) state.interBondsValidUnit = props.parent.interBondsValidUnit;
+
+        if (props.interBondsValidUnitPair) state.interBondsValidUnitPair = props.interBondsValidUnitPair;
+        else if (props.parent) state.interBondsValidUnitPair = props.parent.interBondsValidUnitPair;
+
         if (props.dynamicBonds) state.dynamicBonds = props.dynamicBonds;
         if (props.dynamicBonds) state.dynamicBonds = props.dynamicBonds;
         else if (props.parent) state.dynamicBonds = props.parent.dynamicBonds;
         else if (props.parent) state.dynamicBonds = props.parent.dynamicBonds;
 
 
@@ -1180,7 +1204,7 @@ namespace Structure {
 
 
     /**
     /**
      * Iterate over all unit pairs of a structure and invokes callback for valid units
      * Iterate over all unit pairs of a structure and invokes callback for valid units
-     * and unit pairs if within a max distance.
+     * and unit pairs if their boundaries are within a max distance.
      */
      */
     export function eachUnitPair(structure: Structure, callback: (unitA: Unit, unitB: Unit) => void, props: EachUnitPairProps) {
     export function eachUnitPair(structure: Structure, callback: (unitA: Unit, unitB: Unit) => void, props: EachUnitPairProps) {
         const { maxRadius, validUnit, validUnitPair } = props;
         const { maxRadius, validUnit, validUnitPair } = props;
@@ -1188,15 +1212,19 @@ namespace Structure {
 
 
         const lookup = structure.lookup3d;
         const lookup = structure.lookup3d;
         const imageCenter = Vec3();
         const imageCenter = Vec3();
+        const bbox = Box3D();
+        const rvec = Vec3.create(maxRadius, maxRadius, maxRadius);
 
 
         for (const unit of structure.units) {
         for (const unit of structure.units) {
             if (!validUnit(unit)) continue;
             if (!validUnit(unit)) continue;
 
 
             const bs = unit.boundary.sphere;
             const bs = unit.boundary.sphere;
+            Box3D.expand(bbox, unit.boundary.box, rvec);
             Vec3.transformMat4(imageCenter, bs.center, unit.conformation.operator.matrix);
             Vec3.transformMat4(imageCenter, bs.center, unit.conformation.operator.matrix);
             const closeUnits = lookup.findUnitIndices(imageCenter[0], imageCenter[1], imageCenter[2], bs.radius + maxRadius);
             const closeUnits = lookup.findUnitIndices(imageCenter[0], imageCenter[1], imageCenter[2], bs.radius + maxRadius);
             for (let i = 0; i < closeUnits.count; i++) {
             for (let i = 0; i < closeUnits.count; i++) {
                 const other = structure.units[closeUnits.indices[i]];
                 const other = structure.units[closeUnits.indices[i]];
+                if (!Box3D.overlaps(bbox, other.boundary.box)) continue;
                 if (!validUnit(other) || unit.id >= other.id || !validUnitPair(unit, other)) continue;
                 if (!validUnit(other) || unit.id >= other.id || !validUnitPair(unit, other)) continue;
 
 
                 if (other.elements.length >= unit.elements.length) callback(unit, other);
                 if (other.elements.length >= unit.elements.length) callback(unit, other);

+ 14 - 6
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -21,12 +21,18 @@ import { StructConn } from '../../../../../mol-model-formats/structure/property/
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { Model } from '../../../model';
 import { Model } from '../../../model';
 
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3distance = Vec3.distance;
+const v3set = Vec3.set;
+const v3squaredDistance = Vec3.squaredDistance;
+const v3transformMat4 = Vec3.transformMat4;
+
 const tmpDistVecA = Vec3();
 const tmpDistVecA = Vec3();
 const tmpDistVecB = Vec3();
 const tmpDistVecB = Vec3();
 function getDistance(unitA: Unit.Atomic, indexA: ElementIndex, unitB: Unit.Atomic, indexB: ElementIndex) {
 function getDistance(unitA: Unit.Atomic, indexA: ElementIndex, unitB: Unit.Atomic, indexB: ElementIndex) {
     unitA.conformation.position(indexA, tmpDistVecA);
     unitA.conformation.position(indexA, tmpDistVecA);
     unitB.conformation.position(indexB, tmpDistVecB);
     unitB.conformation.position(indexB, tmpDistVecB);
-    return Vec3.distance(tmpDistVecA, tmpDistVecB);
+    return v3distance(tmpDistVecA, tmpDistVecB);
 }
 }
 
 
 const _imageTransform = Mat4();
 const _imageTransform = Mat4();
@@ -68,22 +74,22 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI = atomsA[_aI];
         const aI = atomsA[_aI];
-        Vec3.set(_imageA, xA[aI], yA[aI], zA[aI]);
-        if (isNotIdentity) Vec3.transformMat4(_imageA, _imageA, imageTransform);
-        if (Vec3.squaredDistance(_imageA, bCenter) > testDistanceSq) continue;
+        v3set(_imageA, xA[aI], yA[aI], zA[aI]);
+        if (isNotIdentity) v3transformMat4(_imageA, _imageA, imageTransform);
+        if (v3squaredDistance(_imageA, bCenter) > testDistanceSq) continue;
 
 
         if (!props.forceCompute && indexPairs) {
         if (!props.forceCompute && indexPairs) {
             const { maxDistance } = indexPairs;
             const { maxDistance } = indexPairs;
             const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
             const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
 
 
             const srcA = sourceIndex.value(aI);
             const srcA = sourceIndex.value(aI);
+            const aeI = getElementIdx(type_symbolA.value(aI));
             for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) {
             for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) {
                 const bI = invertedIndex![b[i]];
                 const bI = invertedIndex![b[i]];
 
 
                 const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex;
                 const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex;
                 if (_bI < 0) continue;
                 if (_bI < 0) continue;
 
 
-                const aeI = getElementIdx(type_symbolA.value(aI));
                 const beI = getElementIdx(type_symbolA.value(bI));
                 const beI = getElementIdx(type_symbolA.value(bI));
 
 
                 const d = distance[i];
                 const d = distance[i];
@@ -191,6 +197,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 }
 }
 
 
 export interface InterBondComputationProps extends BondComputationProps {
 export interface InterBondComputationProps extends BondComputationProps {
+    validUnit: (unit: Unit) => boolean
     validUnitPair: (structure: Structure, unitA: Unit, unitB: Unit) => boolean
     validUnitPair: (structure: Structure, unitA: Unit, unitB: Unit) => boolean
     ignoreWater: boolean
     ignoreWater: boolean
     ignoreIon: boolean
     ignoreIon: boolean
@@ -215,7 +222,7 @@ function findBonds(structure: Structure, props: InterBondComputationProps) {
         findPairBonds(unitA as Unit.Atomic, unitB as Unit.Atomic, props, builder);
         findPairBonds(unitA as Unit.Atomic, unitB as Unit.Atomic, props, builder);
     }, {
     }, {
         maxRadius: props.maxRadius,
         maxRadius: props.maxRadius,
-        validUnit: (unit: Unit) => Unit.isAtomic(unit),
+        validUnit: (unit: Unit) => props.validUnit(unit),
         validUnitPair: (unitA: Unit, unitB: Unit) => props.validUnitPair(structure, unitA, unitB)
         validUnitPair: (unitA: Unit, unitB: Unit) => props.validUnitPair(structure, unitA, unitB)
     });
     });
 
 
@@ -226,6 +233,7 @@ function computeInterUnitBonds(structure: Structure, props?: Partial<InterBondCo
     const p = { ...DefaultInterBondComputationProps, ...props };
     const p = { ...DefaultInterBondComputationProps, ...props };
     return findBonds(structure, {
     return findBonds(structure, {
         ...p,
         ...p,
+        validUnit: (props && props.validUnit) || (u => Unit.isAtomic(u)),
         validUnitPair: (props && props.validUnitPair) || ((s, a, b) => {
         validUnitPair: (props && props.validUnitPair) || ((s, a, b) => {
             const mtA = a.model.atomicHierarchy.derived.residue.moleculeType;
             const mtA = a.model.atomicHierarchy.derived.residue.moleculeType;
             const mtB = b.model.atomicHierarchy.derived.residue.moleculeType;
             const mtB = b.model.atomicHierarchy.derived.residue.moleculeType;

+ 4 - 1
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -21,6 +21,9 @@ import { ElementIndex } from '../../../model/indexing';
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { Model } from '../../../model/model';
 import { Model } from '../../../model/model';
 
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3distance = Vec3.distance;
+
 function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
 function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const flags = new Uint16Array(builder.slotCount);
     const flags = new Uint16Array(builder.slotCount);
@@ -39,7 +42,7 @@ const tmpDistVecB = Vec3();
 function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementIndex) {
 function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementIndex) {
     unit.conformation.position(indexA, tmpDistVecA);
     unit.conformation.position(indexA, tmpDistVecA);
     unit.conformation.position(indexB, tmpDistVecB);
     unit.conformation.position(indexB, tmpDistVecB);
-    return Vec3.distance(tmpDistVecA, tmpDistVecB);
+    return v3distance(tmpDistVecA, tmpDistVecB);
 }
 }
 
 
 const __structConnAdded = new Set<StructureElement.UnitIndex>();
 const __structConnAdded = new Set<StructureElement.UnitIndex>();

+ 21 - 5
src/mol-model/structure/structure/util/superposition-sifts-mapping.ts

@@ -8,7 +8,8 @@
 import { Segmentation } from '../../../../mol-data/int';
 import { Segmentation } from '../../../../mol-data/int';
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import { SIFTSMapping } from '../../../../mol-model-props/sequence/sifts-mapping';
 import { SIFTSMapping } from '../../../../mol-model-props/sequence/sifts-mapping';
-import { ElementIndex } from '../../model/indexing';
+import { ElementIndex, ResidueIndex } from '../../model/indexing';
+import { StructureElement } from '../element';
 import { Structure } from '../structure';
 import { Structure } from '../structure';
 import { Unit } from '../unit';
 import { Unit } from '../unit';
 
 
@@ -24,11 +25,16 @@ export interface AlignmentResult {
     failedPairs: [number, number][]
     failedPairs: [number, number][]
 }
 }
 
 
-export function alignAndSuperposeWithSIFTSMapping(structures: Structure[], options?: { traceOnly?: boolean }): AlignmentResult {
+type IncludeResidueTest = (traceElementOrFirstAtom: StructureElement.Location<Unit.Atomic>, residueIndex: ResidueIndex, startIndex: ElementIndex, endIndex: ElementIndex) => boolean
+
+export function alignAndSuperposeWithSIFTSMapping(
+    structures: Structure[],
+    options?: { traceOnly?: boolean, includeResidueTest?: IncludeResidueTest }
+): AlignmentResult {
     const indexMap = new Map<string, IndexEntry>();
     const indexMap = new Map<string, IndexEntry>();
 
 
     for (let i = 0; i < structures.length; i++) {
     for (let i = 0; i < structures.length; i++) {
-        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true);
+        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true, options?.includeResidueTest ?? _includeAllResidues);
     }
     }
 
 
     const index = Array.from(indexMap.values());
     const index = Array.from(indexMap.values());
@@ -137,11 +143,16 @@ interface IndexEntry {
     pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
     pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
 }
 }
 
 
-function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean) {
+function _includeAllResidues() { return true; }
+
+function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean, includeTest: IncludeResidueTest) {
+    const loc = StructureElement.Location.create<Unit.Atomic>(structure);
+
     for (const unit of structure.units) {
     for (const unit of structure.units) {
         if (unit.kind !== Unit.Kind.Atomic) continue;
         if (unit.kind !== Unit.Kind.Atomic) continue;
 
 
         const { elements, model } = unit;
         const { elements, model } = unit;
+        loc.unit = unit;
 
 
         const map = SIFTSMapping.Provider.get(model).value;
         const map = SIFTSMapping.Provider.get(model).value;
         if (!map) return;
         if (!map) return;
@@ -161,9 +172,11 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
 
 
                 if (!dbName[rI]) continue;
                 if (!dbName[rI]) continue;
 
 
+                const traceElement = traceElementIndex[rI];
+
                 let start, end;
                 let start, end;
                 if (traceOnly) {
                 if (traceOnly) {
-                    start = traceElementIndex[rI];
+                    start = traceElement;
                     if (start === -1) continue;
                     if (start === -1) continue;
                     end = start + 1 as ElementIndex;
                     end = start + 1 as ElementIndex;
                 } else {
                 } else {
@@ -171,6 +184,9 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
                     end = elements[residueSegment.end - 1] + 1 as ElementIndex;
                     end = elements[residueSegment.end - 1] + 1 as ElementIndex;
                 }
                 }
 
 
+                loc.element = (traceElement >= 0 ? traceElement : start) as ElementIndex;
+                if (!includeTest(loc, rI, start, end)) continue;
+
                 const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
                 const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
 
 
                 if (!index.has(key)) {
                 if (!index.has(key)) {

+ 13 - 7
src/mol-plugin-state/actions/file.ts

@@ -83,7 +83,7 @@ export const DownloadFile = StateAction.build({
     display: { name: 'Download File', description: 'Load one or more file from an URL' },
     display: { name: 'Download File', description: 'Load one or more file from an URL' },
     from: PluginStateObject.Root,
     from: PluginStateObject.Root,
     params: (a, ctx: PluginContext) => {
     params: (a, ctx: PluginContext) => {
-        const options = [...ctx.dataFormats.options, ['zip', 'Zip'] as const];
+        const options = [...ctx.dataFormats.options, ['zip', 'Zip'] as const, ['gzip', 'Gzip'] as const];
         return {
         return {
             url: PD.Url(''),
             url: PD.Url(''),
             format: PD.Select(options[0][0], options),
             format: PD.Select(options[0][0], options),
@@ -96,17 +96,23 @@ export const DownloadFile = StateAction.build({
 
 
     await state.transaction(async () => {
     await state.transaction(async () => {
         try {
         try {
-            if (params.format === 'zip') {
+            if (params.format === 'zip' || params.format === 'gzip') {
                 // TODO: add ReadZipFile transformer so this can be saved as a simple state snaphot,
                 // TODO: add ReadZipFile transformer so this can be saved as a simple state snaphot,
                 //       would need support for extracting individual files from zip
                 //       would need support for extracting individual files from zip
                 const data = await plugin.builders.data.download({ url: params.url, isBinary: true });
                 const data = await plugin.builders.data.download({ url: params.url, isBinary: true });
-                const zippedFiles = await unzip(taskCtx, (data.obj?.data as Uint8Array).buffer);
-                for (const [fn, filedata] of Object.entries(zippedFiles)) {
-                    if (!(filedata instanceof Uint8Array) || filedata.length === 0) continue;
+                if (params.format === 'zip') {
+                    const zippedFiles = await unzip(taskCtx, (data.obj?.data as Uint8Array).buffer);
+                    for (const [fn, filedata] of Object.entries(zippedFiles)) {
+                        if (!(filedata instanceof Uint8Array) || filedata.length === 0) continue;
 
 
-                    const asset = Asset.File(new File([filedata], fn));
+                        const asset = Asset.File(new File([filedata], fn));
 
 
-                    await processFile(asset, plugin, 'auto', params.visuals);
+                        await processFile(asset, plugin, 'auto', params.visuals);
+                    }
+                } else {
+                    const url = Asset.getUrl(params.url);
+                    const info = getFileInfo(url);
+                    await processFile(Asset.File(new File([data.obj?.data as Uint8Array], info.name)), plugin, 'auto', params.visuals);
                 }
                 }
             } else {
             } else {
                 const provider = plugin.dataFormats.get(params.format);
                 const provider = plugin.dataFormats.get(params.format);

+ 2 - 1
src/mol-plugin-state/actions/structure.ts

@@ -90,6 +90,7 @@ const DownloadStructure = StateAction.build({
                     url: PD.Url(''),
                     url: PD.Url(''),
                     format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuiltInTrajectoryFormats.map(f => f[0]), f => f)),
                     format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuiltInTrajectoryFormats.map(f => f[0]), f => f)),
                     isBinary: PD.Boolean(false),
                     isBinary: PD.Boolean(false),
+                    label: PD.Optional(PD.Text('')),
                     options
                     options
                 }, { isFlat: true, label: 'URL' })
                 }, { isFlat: true, label: 'URL' })
             })
             })
@@ -104,7 +105,7 @@ const DownloadStructure = StateAction.build({
 
 
     switch (src.name) {
     switch (src.name) {
         case 'url':
         case 'url':
-            downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary }];
+            downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary, label: src.params.label || undefined }];
             format = src.params.format;
             format = src.params.format;
             break;
             break;
         case 'pdb':
         case 'pdb':

+ 25 - 21
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -41,8 +41,10 @@ export namespace StructureRepresentationPresetProvider {
         quality: PD.Optional(PD.Select<VisualQuality>('auto', VisualQualityOptions)),
         quality: PD.Optional(PD.Select<VisualQuality>('auto', VisualQualityOptions)),
         theme: PD.Optional(PD.Group({
         theme: PD.Optional(PD.Group({
             globalName: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
             globalName: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
+            globalColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
             carbonColor: PD.Optional(PD.Select('chain-id', PD.arrayToOptions(['chain-id', 'operator-name', 'element-symbol'] as const))),
             carbonColor: PD.Optional(PD.Select('chain-id', PD.arrayToOptions(['chain-id', 'operator-name', 'element-symbol'] as const))),
             symmetryColor: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
             symmetryColor: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
+            symmetryColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
             focus: PD.Optional(PD.Group({
             focus: PD.Optional(PD.Group({
                 name: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
                 name: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
                 params: PD.Optional(PD.Value<ColorTheme.BuiltInParams<ColorTheme.BuiltIn>>({} as any))
                 params: PD.Optional(PD.Value<ColorTheme.BuiltInParams<ColorTheme.BuiltIn>>({} as any))
@@ -76,13 +78,15 @@ export namespace StructureRepresentationPresetProvider {
         if (params.ignoreLight !== void 0) typeParams.ignoreLight = !!params.ignoreLight;
         if (params.ignoreLight !== void 0) typeParams.ignoreLight = !!params.ignoreLight;
         const color: ColorTheme.BuiltIn | undefined = params.theme?.globalName ? params.theme?.globalName : void 0;
         const color: ColorTheme.BuiltIn | undefined = params.theme?.globalName ? params.theme?.globalName : void 0;
         const ballAndStickColor: ColorTheme.BuiltInParams<'element-symbol'> = params.theme?.carbonColor !== undefined
         const ballAndStickColor: ColorTheme.BuiltInParams<'element-symbol'> = params.theme?.carbonColor !== undefined
-            ? { carbonColor: getCarbonColorParams(params.theme?.carbonColor) }
-            : { };
+            ? { carbonColor: getCarbonColorParams(params.theme?.carbonColor), ...params.theme?.globalColorParams }
+            : { ...params.theme?.globalColorParams };
         const symmetryColor: ColorTheme.BuiltIn | undefined = structure && params.theme?.symmetryColor
         const symmetryColor: ColorTheme.BuiltIn | undefined = structure && params.theme?.symmetryColor
             ? isSymmetry(structure) ? params.theme?.symmetryColor : color
             ? isSymmetry(structure) ? params.theme?.symmetryColor : color
             : color;
             : color;
+        const symmetryColorParams = params.theme?.symmetryColorParams ? { ...params.theme?.globalColorParams, ...params.theme?.symmetryColorParams } : { ...params.theme?.globalColorParams };
+        const globalColorParams = params.theme?.globalColorParams ? { ...params.theme?.globalColorParams } : undefined;
 
 
-        return { update, builder, color, symmetryColor, typeParams, ballAndStickColor };
+        return { update, builder, color, symmetryColor, symmetryColorParams, globalColorParams, typeParams, ballAndStickColor };
     }
     }
 
 
     export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
     export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
@@ -177,18 +181,18 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
         const waterType = (components.water?.obj?.data?.elementCount || 0) > 50_000 ? 'line' : 'ball-and-stick';
         const waterType = (components.water?.obj?.data?.elementCount || 0) > 50_000 ? 'line' : 'ball-and-stick';
         const lipidType = (components.lipid?.obj?.data?.elementCount || 0) > 20_000 ? 'line' : 'ball-and-stick';
         const lipidType = (components.lipid?.obj?.data?.elementCount || 0) > 20_000 ? 'line' : 'ball-and-stick';
 
 
-        const { update, builder, typeParams, color, symmetryColor, ballAndStickColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, color, symmetryColor, symmetryColorParams, globalColorParams, ballAndStickColor } = reprBuilder(plugin, params, structure);
 
 
         const representations = {
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'ligand' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'ligand' }),
             nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'non-standard' }),
             nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'non-standard' }),
             branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color, colorParams: ballAndStickColor }, { tag: 'branched-ball-and-stick' }),
             branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color, colorParams: ballAndStickColor }, { tag: 'branched-ball-and-stick' }),
-            branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color }, { tag: 'branched-snfg-3d' }),
-            water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'water' }),
-            ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ion' }),
-            lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'lipid' }),
-            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id' }, { tag: 'coarse' })
+            branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color, colorParams: globalColorParams }, { tag: 'branched-snfg-3d' }),
+            water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'water' }),
+            ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'ion' }),
+            lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'lipid' }),
+            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id', colorParams: globalColorParams }, { tag: 'coarse' })
         };
         };
 
 
         await update.commit({ revertOnError: false });
         await update.commit({ revertOnError: false });
@@ -223,11 +227,11 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
             smoothness: structure.isCoarseGrained ? 1.0 : 1.5,
             smoothness: structure.isCoarseGrained ? 1.0 : 1.5,
         };
         };
 
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
 
         const representations = {
         const representations = {
-            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'protein' }),
-            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'nucleic' })
+            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'protein' }),
+            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'nucleic' })
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -275,11 +279,11 @@ const coarseSurface = StructureRepresentationPresetProvider({
             });
             });
         }
         }
 
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
 
         const representations = {
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'polymer' }),
-            lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'lipid' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
+            lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'lipid' })
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -309,10 +313,10 @@ const polymerCartoon = StructureRepresentationPresetProvider({
             sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
             sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
         };
         };
 
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
 
         const representations = {
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' })
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -367,9 +371,9 @@ const atomicDetail = StructureRepresentationPresetProvider({
             });
             });
         }
         }
 
 
-        const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, color, ballAndStickColor, globalColorParams } = reprBuilder(plugin, params, structure);
         const colorParams = lowResidueElementRatio && !bondsGiven
         const colorParams = lowResidueElementRatio && !bondsGiven
-            ? { carbonColor: { name: 'element-symbol', params: {} } }
+            ? { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams }
             : ballAndStickColor;
             : ballAndStickColor;
 
 
         const representations = {
         const representations = {
@@ -377,7 +381,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
         };
         };
         if (showCarbohydrateSymbol) {
         if (showCarbohydrateSymbol) {
             Object.assign(representations, {
             Object.assign(representations, {
-                snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color }, { tag: 'snfg-3d' }),
+                snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color, colorParams: globalColorParams }, { tag: 'snfg-3d' }),
             });
             });
         }
         }
 
 

+ 28 - 7
src/mol-plugin-ui/custom/volume.tsx

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Adam Midlik <midlik@gmail.com>
  */
  */
 
 
 import { PluginUIComponent } from '../base';
 import { PluginUIComponent } from '../base';
@@ -199,6 +200,9 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
             const viewParams = { ...oldView };
             const viewParams = { ...oldView };
             if (value.name === 'selection-box') {
             if (value.name === 'selection-box') {
                 viewParams.radius = value.params.radius;
                 viewParams.radius = value.params.radius;
+            } else if (value.name === 'camera-target') {
+                viewParams.radius = value.params.radius;
+                viewParams.dynamicDetailLevel = value.params.dynamicDetailLevel;
             } else if (value.name === 'box') {
             } else if (value.name === 'box') {
                 viewParams.bottomLeft = value.params.bottomLeft;
                 viewParams.bottomLeft = value.params.bottomLeft;
                 viewParams.topRight = value.params.topRight;
                 viewParams.topRight = value.params.topRight;
@@ -240,13 +244,23 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
         const pivot = isEM ? 'em' : '2fo-fc';
         const pivot = isEM ? 'em' : '2fo-fc';
 
 
         const params = this.props.params as VolumeStreaming.Params;
         const params = this.props.params as VolumeStreaming.Params;
-        const entry = ((this.props.info.params as VolumeStreaming.ParamDefinition)
-            .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>);
+        const entry = (this.props.info.params as VolumeStreaming.ParamDefinition)
+            .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>;
         const detailLevel = entry.params.detailLevel;
         const detailLevel = entry.params.detailLevel;
-        const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
+        const dynamicDetailLevel = {
+            ...detailLevel,
+            label: 'Dynamic Detail',
+            defaultValue: (entry.params.view as any).map('camera-target').params.dynamicDetailLevel.defaultValue,
+        };
+        const selectionDetailLevel = {
+            ...detailLevel,
+            label: 'Selection Detail',
+            defaultValue: (entry.params.view as any).map('auto').params.selectionDetailLevel.defaultValue,
+        };
 
 
         const sampling = b.info.header.sampling[0];
         const sampling = b.info.header.sampling[0];
 
 
+        const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
         const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
         const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
 
 
         const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
         const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
@@ -274,6 +288,13 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                     isRelative: isRelativeParam,
                     isRelative: isRelativeParam,
                     isUnbounded: isUnboundedParam,
                     isUnbounded: isUnboundedParam,
                 }, { description: 'Box around focused element.' }),
                 }, { description: 'Box around focused element.' }),
+                'camera-target': PD.Group({
+                    radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
+                    detailLevel: { ...detailLevel, isHidden: true },
+                    dynamicDetailLevel: dynamicDetailLevel,
+                    isRelative: isRelativeParam,
+                    isUnbounded: isUnboundedParam,
+                }, { description: 'Box around camera target.' }),
                 'cell': PD.Group({
                 'cell': PD.Group({
                     detailLevel,
                     detailLevel,
                     isRelative: isRelativeParam,
                     isRelative: isRelativeParam,
@@ -282,12 +303,11 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                 'auto': PD.Group({
                 'auto': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
                     detailLevel,
                     detailLevel,
-                    selectionDetailLevel: { ...detailLevel, label: 'Selection Detail' },
+                    selectionDetailLevel: selectionDetailLevel,
                     isRelative: isRelativeParam,
                     isRelative: isRelativeParam,
                     isUnbounded: isUnboundedParam,
                     isUnbounded: isUnboundedParam,
                 }, { description: 'Box around focused element.' }),
                 }, { description: 'Box around focused element.' }),
-                // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
-            }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Whole Structure" shows the volume for the whole structure.' })
+            }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Around Camera" shows the volume around the point the camera is targeting. "Whole Structure" shows the volume for the whole structure.' })
         };
         };
         const options = {
         const options = {
             entry: params.entry.name,
             entry: params.entry.name,
@@ -299,6 +319,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                     bottomLeft: (params.entry.params.view.params as any).bottomLeft,
                     bottomLeft: (params.entry.params.view.params as any).bottomLeft,
                     topRight: (params.entry.params.view.params as any).topRight,
                     topRight: (params.entry.params.view.params as any).topRight,
                     selectionDetailLevel: (params.entry.params.view.params as any).selectionDetailLevel,
                     selectionDetailLevel: (params.entry.params.view.params as any).selectionDetailLevel,
+                    dynamicDetailLevel: (params.entry.params.view.params as any).dynamicDetailLevel,
                     isRelative,
                     isRelative,
                     isUnbounded
                     isUnbounded
                 }
                 }

+ 21 - 9
src/mol-plugin-ui/viewport/help.tsx

@@ -7,14 +7,15 @@
 import * as React from 'react';
 import * as React from 'react';
 import { Binding } from '../../mol-util/binding';
 import { Binding } from '../../mol-util/binding';
 import { PluginUIComponent } from '../base';
 import { PluginUIComponent } from '../base';
-import { StateTransformer, StateSelection } from '../../mol-state';
+import { StateTransformer, StateSelection, State } from '../../mol-state';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
 import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
 import { Button } from '../controls/common';
 import { Button } from '../controls/common';
+import { memoizeLatest } from '../../mol-util/memoize';
 
 
 function getBindingsList(bindings: { [k: string]: Binding }) {
 function getBindingsList(bindings: { [k: string]: Binding }) {
-    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]);
+    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]).filter(b => Binding.isBinding(b[1]));
 }
 }
 
 
 export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
 export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
@@ -77,19 +78,30 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
     }
     }
 
 
-    render() {
-        const interactionBindings: { [k: string]: Binding } = {};
-        this.plugin.spec.behaviors.forEach(b => {
-            const { bindings } = b.defaultParams;
-            if (bindings) Object.assign(interactionBindings, bindings);
+    getInteractionBindings = memoizeLatest((cells: State.Cells) => {
+        let interactionBindings: { [k: string]: Binding } | undefined = void 0;
+
+        cells.forEach(c => {
+            const params = c.params?.values;
+            if (params?.bindings && Object.keys(params.bindings).length > 0) {
+                if (!interactionBindings) interactionBindings = { };
+                Object.assign(interactionBindings, params.bindings);
+            }
         });
         });
+
+        return interactionBindings;
+    });
+
+    render() {
+        const interactionBindings = this.getInteractionBindings(this.plugin.state.behaviors.cells);
+
         return <>
         return <>
             {(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
             {(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
                 <BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
                 <BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
             </HelpGroup>}
             </HelpGroup>}
-            <HelpGroup key='interactions' header='Mouse Controls'>
+            {!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
                 <BindingsHelp bindings={interactionBindings} />
                 <BindingsHelp bindings={interactionBindings} />
-            </HelpGroup>
+            </HelpGroup>}
         </>;
         </>;
     }
     }
 }
 }

+ 14 - 2
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -8,8 +8,10 @@
 import { produce } from 'immer';
 import { produce } from 'immer';
 import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
 import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginConfig } from '../../mol-plugin/config';
 import { StateTransform } from '../../mol-state';
 import { StateTransform } from '../../mol-state';
 import { Color } from '../../mol-util/color';
 import { Color } from '../../mol-util/color';
+import { deepClone } from '../../mol-util/object';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamMapping } from '../../mol-util/param-mapping';
 import { ParamMapping } from '../../mol-util/param-mapping';
 import { Mutable } from '../../mol-util/type-helpers';
 import { Mutable } from '../../mol-util/type-helpers';
@@ -50,7 +52,8 @@ const SimpleSettingsParams = {
     camera: Canvas3DParams.camera,
     camera: Canvas3DParams.camera,
     background: PD.Group({
     background: PD.Group({
         color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }),
         color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }),
-        transparent: PD.Boolean(false)
+        transparent: PD.Boolean(false),
+        style: Canvas3DParams.postprocessing.params.background,
     }, { pivot: 'color' }),
     }, { pivot: 'color' }),
     lighting: PD.Group({
     lighting: PD.Group({
         occlusion: Canvas3DParams.postprocessing.params.occlusion,
         occlusion: Canvas3DParams.postprocessing.params.occlusion,
@@ -75,6 +78,13 @@ const SimpleSettingsMapping = ParamMapping({
             if (controls.left !== 'none') options.push(['left', LayoutOptions.left]);
             if (controls.left !== 'none') options.push(['left', LayoutOptions.left]);
             params.layout.options = options;
             params.layout.options = options;
         }
         }
+        const bgStyles = ctx.config.get(PluginConfig.Background.Styles) || [];
+        if (bgStyles.length > 0) {
+            Object.assign(params.background.params.style, {
+                presets: deepClone(bgStyles),
+                isFlat: false, // so the presets menu is shown
+            });
+        }
         return params;
         return params;
     },
     },
     target(ctx: PluginUIContext) {
     target(ctx: PluginUIContext) {
@@ -97,7 +107,8 @@ const SimpleSettingsMapping = ParamMapping({
             camera: canvas.camera,
             camera: canvas.camera,
             background: {
             background: {
                 color: renderer.backgroundColor,
                 color: renderer.backgroundColor,
-                transparent: canvas.transparentBackground
+                transparent: canvas.transparentBackground,
+                style: canvas.postprocessing.background,
             },
             },
             lighting: {
             lighting: {
                 occlusion: canvas.postprocessing.occlusion,
                 occlusion: canvas.postprocessing.occlusion,
@@ -117,6 +128,7 @@ const SimpleSettingsMapping = ParamMapping({
         canvas.renderer.backgroundColor = s.background.color;
         canvas.renderer.backgroundColor = s.background.color;
         canvas.postprocessing.occlusion = s.lighting.occlusion;
         canvas.postprocessing.occlusion = s.lighting.occlusion;
         canvas.postprocessing.outline = s.lighting.outline;
         canvas.postprocessing.outline = s.lighting.outline;
+        canvas.postprocessing.background = s.background.style;
         canvas.cameraFog = s.lighting.fog;
         canvas.cameraFog = s.lighting.fog;
         canvas.cameraClipping = {
         canvas.cameraClipping = {
             radius: s.clipping.radius,
             radius: s.clipping.radius,

+ 14 - 3
src/mol-plugin/behavior/behavior.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Adam Midlik <midlik@gmail.com>
  */
  */
 
 
 import { PluginStateTransform, PluginStateObject } from '../../mol-plugin-state/objects';
 import { PluginStateTransform, PluginStateObject } from '../../mol-plugin-state/objects';
@@ -144,8 +145,18 @@ namespace PluginBehavior {
         protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
         protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
             this.subs.push(cmd.subscribe(this.plugin, action));
             this.subs.push(cmd.subscribe(this.plugin, action));
         }
         }
-        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
-            this.subs.push(o.subscribe(action));
+        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void): PluginCommand.Subscription {
+            const sub = o.subscribe(action);
+            this.subs.push(sub);
+            return {
+                unsubscribe: () => {
+                    const idx = this.subs.indexOf(sub);
+                    if (idx >= 0) {
+                        this.subs.splice(idx, 1);
+                        sub.unsubscribe();
+                    }
+                }
+            };
         }
         }
         dispose(): void {
         dispose(): void {
             for (const s of this.subs) s.unsubscribe();
             for (const s of this.subs) s.unsubscribe();

+ 163 - 84
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -1,8 +1,9 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Adam Midlik <midlik@gmail.com>
  */
  */
 
 
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
@@ -24,6 +25,10 @@ import { PluginContext } from '../../../context';
 import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
 import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
 import { Asset } from '../../../../mol-util/assets';
 import { Asset } from '../../../../mol-util/assets';
 import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform';
 import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform';
+import { distinctUntilChanged, filter, map, Observable, throttleTime } from 'rxjs';
+import { Camera } from '../../../../mol-canvas3d/camera';
+import { PluginCommand } from '../../../command';
+import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
 
 
 export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
 export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
 
 
@@ -53,7 +58,7 @@ export namespace VolumeStreaming {
         valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
         valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
     };
     };
 
 
-    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = { }) {
+    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = {}) {
         const { data, defaultView, channelParams } = options;
         const { data, defaultView, channelParams } = options;
         const map = new Map<string, VolumeServerInfo.EntryData>();
         const map = new Map<string, VolumeServerInfo.EntryData>();
         if (data) data.entries.forEach(d => map.set(d.dataId, d));
         if (data) data.entries.forEach(d => map.set(d.dataId, d));
@@ -68,7 +73,7 @@ export namespace VolumeStreaming {
     export type EntryParams = PD.Values<EntryParamDefinition>
     export type EntryParams = PD.Values<EntryParamDefinition>
 
 
     export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
     export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
-        const { entryData, defaultView, structure, channelParams = { } } = options;
+        const { entryData, defaultView, structure, channelParams = {} } = options;
 
 
         // fake the info
         // fake the info
         const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
         const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
@@ -86,19 +91,24 @@ export namespace VolumeStreaming {
                     bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                     bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                     topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                     topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                 }, { description: 'Box around focused element.', isFlat: true }),
                 }, { description: 'Box around focused element.', isFlat: true }),
+                'camera-target': PD.Group({
+                    radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
+                    // Minimal detail level for the inside of the zoomed region (real detail can be higher, depending on the region size)
+                    dynamicDetailLevel: createDetailParams(info.header.availablePrecisions, 0, { label: 'Dynamic Detail' }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                }, { description: 'Box around camera target.', isFlat: true }),
                 'cell': PD.Group<{}>({}),
                 'cell': PD.Group<{}>({}),
                 // Show selection-box if available and cell otherwise.
                 // Show selection-box if available and cell otherwise.
                 'auto': PD.Group({
                 'auto': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
-                    selectionDetailLevel: PD.Select<number>(Math.min(6, info.header.availablePrecisions.length - 1),
-                        info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { label: 'Selection Detail', description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
+                    selectionDetailLevel: createDetailParams(info.header.availablePrecisions, 6, { label: 'Selection Detail' }),
                     isSelection: PD.Boolean(false, { isHidden: true }),
                     isSelection: PD.Boolean(false, { isHidden: true }),
                     bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
                     bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
                     topRight: PD.Vec3(box.max, {}, { isHidden: true }),
                     topRight: PD.Vec3(box.max, {}, { isHidden: true }),
                 }, { description: 'Box around focused element.', isFlat: true })
                 }, { description: 'Box around focused element.', isFlat: true })
             }, { options: ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the focused element/atom. "Whole Structure" shows the volume for the whole structure.' }),
             }, { options: ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the focused element/atom. "Whole Structure" shows the volume for the whole structure.' }),
-            detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
-                info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
+            detailLevel: createDetailParams(info.header.availablePrecisions, 3),
             channels: info.kind === 'em'
             channels: info.kind === 'em'
                 ? PD.Group({
                 ? PD.Group({
                     'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
                     'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
@@ -111,13 +121,40 @@ export namespace VolumeStreaming {
         };
         };
     }
     }
 
 
-    export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
+    function createDetailParams(availablePrecisions: VolumeServerHeader.DetailLevel[], preferredPrecision: number, info?: PD.Info) {
+        return PD.Select<number>(Math.min(preferredPrecision, availablePrecisions.length - 1),
+            availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]),
+            {
+                description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 1 (0.52M voxels) to 7 (25.17M voxels).',
+                ...info
+            }
+        );
+    }
 
 
-    export type ViewTypes = 'off' | 'box' | 'selection-box' | 'cell' | 'auto'
+    export function copyParams(origParams: Params): Params {
+        return {
+            entry: {
+                name: origParams.entry.name,
+                params: {
+                    detailLevel: origParams.entry.params.detailLevel,
+                    channels: origParams.entry.params.channels,
+                    view: {
+                        name: origParams.entry.params.view.name,
+                        params: { ...origParams.entry.params.view.params } as any,
+                    }
+                }
+            }
+        };
+    }
+
+    export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['camera-target', 'Around Camera'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
+
+    export type ViewTypes = 'off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'
 
 
     export type ParamDefinition = ReturnType<typeof createParams>
     export type ParamDefinition = ReturnType<typeof createParams>
     export type Params = PD.Values<ParamDefinition>
     export type Params = PD.Values<ParamDefinition>
 
 
+
     type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
     type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
     type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
     type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
 
 
@@ -140,6 +177,14 @@ export namespace VolumeStreaming {
         private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
         private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
         private ref: string = '';
         private ref: string = '';
         public infoMap: Map<string, VolumeServerInfo.EntryData>;
         public infoMap: Map<string, VolumeServerInfo.EntryData>;
+        private updateQueue: SingleAsyncQueue;
+        private cameraTargetObservable = this.plugin.canvas3d!.didDraw!.pipe(
+            throttleTime(500, undefined, { 'leading': true, 'trailing': true }),
+            map(() => this.plugin.canvas3d?.camera.getSnapshot()),
+            distinctUntilChanged((a, b) => this.isCameraTargetSame(a, b)),
+            filter(a => a !== undefined),
+        ) as Observable<Camera.Snapshot>;
+        private cameraTargetSubscription?: PluginCommand.Subscription = undefined;
 
 
         channels: Channels = {};
         channels: Channels = {};
 
 
@@ -163,6 +208,9 @@ export namespace VolumeStreaming {
             if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
             if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
                 detail = this.params.entry.params.view.params.selectionDetailLevel;
                 detail = this.params.entry.params.view.params.selectionDetailLevel;
             }
             }
+            if (this.params.entry.params.view.name === 'camera-target' && box) {
+                detail = this.decideDetail(box, this.params.entry.params.view.params.dynamicDetailLevel);
+            }
 
 
             url += `?detail=${detail}`;
             url += `?detail=${detail}`;
 
 
@@ -201,58 +249,21 @@ export namespace VolumeStreaming {
             return ret;
             return ret;
         }
         }
 
 
-        private updateSelectionBoxParams(box: Box3D) {
-            if (this.params.entry.params.view.name !== 'selection-box') return;
-
-            const state = this.plugin.state.data;
-            const newParams: Params = {
-                ...this.params,
-                entry: {
-                    name: this.params.entry.name,
-                    params: {
-                        ...this.params.entry.params,
-                        view: {
-                            name: 'selection-box' as const,
-                            params: {
-                                radius: this.params.entry.params.view.params.radius,
-                                bottomLeft: box.min,
-                                topRight: box.max
-                            }
-                        }
-                    }
-                }
-            };
-            const update = state.build().to(this.ref).update(newParams);
-
-            PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
-        }
-
-        private updateAutoParams(box: Box3D | undefined, isSelection: boolean) {
-            if (this.params.entry.params.view.name !== 'auto') return;
+        private async updateParams(box: Box3D | undefined, autoIsSelection: boolean = false) {
+            const newParams = copyParams(this.params);
+            const viewType = newParams.entry.params.view.name;
+            if (viewType !== 'off' && viewType !== 'cell') {
+                newParams.entry.params.view.params.bottomLeft = box?.min || Vec3.zero();
+                newParams.entry.params.view.params.topRight = box?.max || Vec3.zero();
+            }
+            if (viewType === 'auto') {
+                newParams.entry.params.view.params.isSelection = autoIsSelection;
+            }
 
 
             const state = this.plugin.state.data;
             const state = this.plugin.state.data;
-            const newParams: Params = {
-                ...this.params,
-                entry: {
-                    name: this.params.entry.name,
-                    params: {
-                        ...this.params.entry.params,
-                        view: {
-                            name: 'auto' as const,
-                            params: {
-                                radius: this.params.entry.params.view.params.radius,
-                                selectionDetailLevel: this.params.entry.params.view.params.selectionDetailLevel,
-                                isSelection,
-                                bottomLeft: box?.min || Vec3.zero(),
-                                topRight: box?.max || Vec3.zero()
-                            }
-                        }
-                    }
-                }
-            };
             const update = state.build().to(this.ref).update(newParams);
             const update = state.build().to(this.ref).update(newParams);
 
 
-            PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
+            await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
         }
         }
 
 
         private getStructureRoot() {
         private getStructureRoot() {
@@ -303,6 +314,18 @@ export namespace VolumeStreaming {
             }
             }
         }
         }
 
 
+        private isCameraTargetSame(a?: Camera.Snapshot, b?: Camera.Snapshot): boolean {
+            if (!a || !b) return false;
+            const targetSame = Vec3.equals(a.target, b.target);
+            const sqDistA = Vec3.squaredDistance(a.target, a.position);
+            const sqDistB = Vec3.squaredDistance(b.target, b.position);
+            const distanceSame = Math.abs(sqDistA - sqDistB) / sqDistA < 1e-3;
+            return targetSame && distanceSame;
+        }
+        private cameraTargetDistance(snapshot: Camera.Snapshot): number {
+            return Vec3.distance(snapshot.target, snapshot.position);
+        }
+
         private _invTransform: Mat4 = Mat4();
         private _invTransform: Mat4 = Mat4();
         private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
         private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
             if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
             if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
@@ -328,39 +351,82 @@ export namespace VolumeStreaming {
         }
         }
 
 
         private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
         private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
-            // if (Loci.areEqual(this.lastLoci, loci)) {
-            //     this.lastLoci = EmptyLoci;
-            //     this.updateSelectionBoxParams(Box3D.empty());
-            //     return;
-            // }
-
-            this.lastLoci = loci;
-
-            if (isEmptyLoci(loci)) {
-                this.updateAutoParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
-                return;
-            }
-
-            const box = this.getBoxFromLoci(loci);
-            this.updateAutoParams(box, true);
+            this.updateQueue.enqueue(async () => {
+                this.lastLoci = loci;
+                if (isEmptyLoci(loci)) {
+                    await this.updateParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
+                } else {
+                    await this.updateParams(this.getBoxFromLoci(loci), true);
+                }
+            });
         }
         }
 
 
         private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
         private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
-            if (Loci.areEqual(this.lastLoci, loci)) {
-                this.lastLoci = EmptyLoci;
-                this.updateSelectionBoxParams(Box3D());
-                return;
-            }
+            this.updateQueue.enqueue(async () => {
+                if (Loci.areEqual(this.lastLoci, loci)) {
+                    this.lastLoci = EmptyLoci;
+                } else {
+                    this.lastLoci = loci;
+                }
+                const box = this.getBoxFromLoci(this.lastLoci);
+                await this.updateParams(box);
+            });
+        }
 
 
-            this.lastLoci = loci;
+        private updateCameraTarget(snapshot: Camera.Snapshot) {
+            this.updateQueue.enqueue(async () => {
+                const origManualReset = this.plugin.canvas3d?.props.camera.manualReset;
+                try {
+                    if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: true } });
+                    const box = this.boxFromCameraTarget(snapshot, true);
+                    await this.updateParams(box);
+                } finally {
+                    if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: origManualReset } });
+                }
+            });
+        }
 
 
-            if (isEmptyLoci(loci)) {
-                this.updateSelectionBoxParams(Box3D());
-                return;
+        private boxFromCameraTarget(snapshot: Camera.Snapshot, boundByBoundarySize: boolean): Box3D {
+            const target = snapshot.target;
+            const distance = this.cameraTargetDistance(snapshot);
+            const top = Math.tan(0.5 * snapshot.fov) * distance;
+            let radius = top;
+            const viewport = this.plugin.canvas3d?.camera.viewport;
+            if (viewport && viewport.width > viewport.height) {
+                radius *= viewport.width / viewport.height;
+            }
+            const relativeRadius = this.params.entry.params.view.name === 'camera-target' ? this.params.entry.params.view.params.radius : 0.5;
+            radius *= relativeRadius;
+            let radiusX, radiusY, radiusZ;
+            if (boundByBoundarySize) {
+                const bBoxSize = Vec3.zero();
+                Box3D.size(bBoxSize, this.data.structure.boundary.box);
+                radiusX = Math.min(radius, 0.5 * bBoxSize[0]);
+                radiusY = Math.min(radius, 0.5 * bBoxSize[1]);
+                radiusZ = Math.min(radius, 0.5 * bBoxSize[2]);
+            } else {
+                radiusX = radiusY = radiusZ = radius;
             }
             }
+            return Box3D.create(
+                Vec3.create(target[0] - radiusX, target[1] - radiusY, target[2] - radiusZ),
+                Vec3.create(target[0] + radiusX, target[1] + radiusY, target[2] + radiusZ)
+            );
+        }
 
 
-            const box = this.getBoxFromLoci(loci);
-            this.updateSelectionBoxParams(box);
+        private decideDetail(box: Box3D, baseDetail: number): number {
+            const cellVolume = this.info.kind === 'x-ray'
+                ? Box3D.volume(this.data.structure.boundary.box)
+                : this.info.header.spacegroup.size.reduce((a, b) => a * b, 1);
+            const boxVolume = Box3D.volume(box);
+            let ratio = boxVolume / cellVolume;
+            const maxDetail = this.info.header.availablePrecisions.length - 1;
+            let detail = baseDetail;
+            while (ratio <= 0.5 && detail < maxDetail) {
+                ratio *= 2;
+                detail += 1;
+            }
+            // console.log(`Decided dynamic detail: ${detail}, (base detail: ${baseDetail}, box/cell volume ratio: ${boxVolume / cellVolume})`);
+            return detail;
         }
         }
 
 
         async update(params: Params) {
         async update(params: Params) {
@@ -369,6 +435,11 @@ export namespace VolumeStreaming {
             this.params = params;
             this.params = params;
             let box: Box3D | undefined = void 0, emptyData = false;
             let box: Box3D | undefined = void 0, emptyData = false;
 
 
+            if (params.entry.params.view.name !== 'camera-target' && this.cameraTargetSubscription) {
+                this.cameraTargetSubscription.unsubscribe();
+                this.cameraTargetSubscription = undefined;
+            }
+
             switch (params.entry.params.view.name) {
             switch (params.entry.params.view.name) {
                 case 'off':
                 case 'off':
                     emptyData = true;
                     emptyData = true;
@@ -388,6 +459,12 @@ export namespace VolumeStreaming {
                     Box3D.expand(box, box, Vec3.create(r, r, r));
                     Box3D.expand(box, box, Vec3.create(r, r, r));
                     break;
                     break;
                 }
                 }
+                case 'camera-target':
+                    if (!this.cameraTargetSubscription) {
+                        this.cameraTargetSubscription = this.subscribeObservable(this.cameraTargetObservable, (e) => this.updateCameraTarget(e));
+                    }
+                    box = this.boxFromCameraTarget(this.plugin.canvas3d!.camera.getSnapshot(), true);
+                    break;
                 case 'cell':
                 case 'cell':
                     box = this.info.kind === 'x-ray'
                     box = this.info.kind === 'x-ray'
                         ? this.data.structure.boundary.box
                         ? this.data.structure.boundary.box
@@ -439,6 +516,7 @@ export namespace VolumeStreaming {
 
 
         getDescription() {
         getDescription() {
             if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
             if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
+            if (this.params.entry.params.view.name === 'camera-target') return 'Camera';
             if (this.params.entry.params.view.name === 'box') return 'Static Box';
             if (this.params.entry.params.view.name === 'box') return 'Static Box';
             if (this.params.entry.params.view.name === 'cell') return 'Cell';
             if (this.params.entry.params.view.name === 'cell') return 'Cell';
             return '';
             return '';
@@ -449,6 +527,7 @@ export namespace VolumeStreaming {
 
 
             this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
             this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
             this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
             this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
+            this.updateQueue = new SingleAsyncQueue();
         }
         }
     }
     }
-}
+}

+ 3 - 1
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -1,8 +1,9 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Adam Midlik <midlik@gmail.com>
  */
  */
 
 
 import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
 import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
@@ -219,6 +220,7 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
     canAutoUpdate: ({ oldParams, newParams }) => {
     canAutoUpdate: ({ oldParams, newParams }) => {
         return oldParams.entry.params.view === newParams.entry.params.view
         return oldParams.entry.params.view === newParams.entry.params.view
             || newParams.entry.params.view.name === 'selection-box'
             || newParams.entry.params.view.name === 'selection-box'
+            || newParams.entry.params.view.name === 'camera-target'
             || newParams.entry.params.view.name === 'off';
             || newParams.entry.params.view.name === 'off';
     },
     },
     apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
     apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {

+ 6 - 2
src/mol-plugin/config.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -12,6 +12,7 @@ import { EmdbDownloadProvider } from '../mol-plugin-state/actions/volume';
 import { StructureRepresentationPresetProvider } from '../mol-plugin-state/builder/structure/representation-preset';
 import { StructureRepresentationPresetProvider } from '../mol-plugin-state/builder/structure/representation-preset';
 import { PluginFeatureDetection } from './features';
 import { PluginFeatureDetection } from './features';
 import { SaccharideCompIdMapType } from '../mol-model/structure/structure/carbohydrates/constants';
 import { SaccharideCompIdMapType } from '../mol-model/structure/structure/carbohydrates/constants';
+import { BackgroundProps } from '../mol-canvas3d/passes/background';
 
 
 export class PluginConfigItem<T = any> {
 export class PluginConfigItem<T = any> {
     toString() { return this.key; }
     toString() { return this.key; }
@@ -30,7 +31,7 @@ export const PluginConfig = {
         PixelScale: item('plugin-config.pixel-scale', 1),
         PixelScale: item('plugin-config.pixel-scale', 1),
         PickScale: item('plugin-config.pick-scale', 0.25),
         PickScale: item('plugin-config.pick-scale', 0.25),
         PickPadding: item('plugin-config.pick-padding', 3),
         PickPadding: item('plugin-config.pick-padding', 3),
-        EnableWboit: item('plugin-config.enable-wboit', PluginFeatureDetection.wboit),
+        EnableWboit: item('plugin-config.enable-wboit', true),
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // TODO: check back in a few weeks to see if it was fixed
         // TODO: check back in a few weeks to see if it was fixed
         PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
         PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
@@ -65,6 +66,9 @@ export const PluginConfig = {
         DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'),
         DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'),
         DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }),
         DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }),
         SaccharideCompIdMapType: item<SaccharideCompIdMapType>('structure.saccharide-comp-id-map-type', 'default'),
         SaccharideCompIdMapType: item<SaccharideCompIdMapType>('structure.saccharide-comp-id-map-type', 'default'),
+    },
+    Background: {
+        Styles: item<[BackgroundProps, string][]>('background.styles', []),
     }
     }
 };
 };
 
 

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

@@ -201,7 +201,7 @@ export class PluginContext {
                 const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1;
                 const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1;
                 const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
                 const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
                 const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false;
                 const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false;
-                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 });
+                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, this.managers.asset, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 });
             }
             }
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             this.canvas3dInit.next(true);
             this.canvas3dInit.next(true);

+ 3 - 8
src/mol-plugin/features.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 export const PluginFeatureDetection = {
 export const PluginFeatureDetection = {
@@ -13,7 +14,7 @@ export const PluginFeatureDetection = {
         const unpportedSafariVersions = [
         const unpportedSafariVersions = [
             'Version/15.1 Safari',
             'Version/15.1 Safari',
             'Version/15.2 Safari',
             'Version/15.2 Safari',
-            'Version/15.3 Safari'
+            'Version/15.3 Safari',
         ];
         ];
         if (unpportedSafariVersions.some(v => navigator.userAgent.indexOf(v) > 0)) {
         if (unpportedSafariVersions.some(v => navigator.userAgent.indexOf(v) > 0)) {
             return true;
             return true;
@@ -28,10 +29,4 @@ export const PluginFeatureDetection = {
         const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
         const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
         return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen));
         return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen));
     },
     },
-    get wboit() {
-        if (typeof navigator === 'undefined' || typeof window === 'undefined') return true;
-
-        // disable Wboit in Safari 15
-        return !/Version\/15.\d Safari/.test(navigator.userAgent);
-    }
 };
 };

+ 3 - 1
src/mol-plugin/util/viewport-screenshot.ts

@@ -309,7 +309,9 @@ class ViewportScreenshotHelper extends PluginComponent {
         if (width <= 0 || height <= 0) return;
         if (width <= 0 || height <= 0) return;
 
 
         await ctx.update('Rendering image...');
         await ctx.update('Rendering image...');
-        const imageData = this.imagePass.getImageData(width, height, viewport);
+        const pass = this.imagePass;
+        await pass.updateBackground();
+        const imageData = pass.getImageData(width, height, viewport);
 
 
         await ctx.update('Encoding image...');
         await ctx.update('Encoding image...');
         const canvas = this.canvas;
         const canvas = this.canvas;

+ 4 - 0
src/mol-util/binding.ts

@@ -24,6 +24,10 @@ namespace Binding {
         return { triggers, action, description };
         return { triggers, action, description };
     }
     }
 
 
+    export function isBinding(x: any): x is Binding {
+        return !!x && Array.isArray(x.triggers) && typeof x.action === 'string';
+    }
+
     export const Empty: Binding = { triggers: [], action: '', description: '' };
     export const Empty: Binding = { triggers: [], action: '', description: '' };
     export function isEmpty(binding: Binding) {
     export function isEmpty(binding: Binding) {
         return binding.triggers.length === 0 ||
         return binding.triggers.length === 0 ||

+ 41 - 0
src/mol-util/single-async-queue.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Adam Midlik <midlik@gmail.com>
+ */
+
+
+/** Job queue that allows at most one running and one pending job.
+ * A newly enqueued job will cancel any other pending jobs. */
+export class SingleAsyncQueue {
+    private isRunning: boolean;
+    private queue: { id: number, func: () => any }[];
+    private counter: number;
+    private log: boolean;
+    constructor(log: boolean = false) {
+        this.isRunning = false;
+        this.queue = [];
+        this.counter = 0;
+        this.log = log;
+    }
+    enqueue(job: () => any) {
+        if (this.log) console.log('SingleAsyncQueue enqueue', this.counter);
+        this.queue[0] = { id: this.counter, func: job };
+        this.counter++;
+        this.run(); // do not await
+    }
+    private async run() {
+        if (this.isRunning) return;
+        const job = this.queue.pop();
+        if (!job) return;
+        this.isRunning = true;
+        try {
+            if (this.log) console.log('SingleAsyncQueue run', job.id);
+            await job.func();
+            if (this.log) console.log('SingleAsyncQueue complete', job.id);
+        } finally {
+            this.isRunning = false;
+            this.run();
+        }
+    }
+}

+ 4 - 1
src/tests/browser/marching-cubes.ts

@@ -22,6 +22,7 @@ import { Representation } from '../../mol-repr/representation';
 import { computeMarchingCubesMesh } from '../../mol-geo/util/marching-cubes/algorithm';
 import { computeMarchingCubesMesh } from '../../mol-geo/util/marching-cubes/algorithm';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -31,7 +32,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
     renderer: { backgroundColor: ColorNames.white },
     renderer: { backgroundColor: ColorNames.white },
     camera: { mode: 'orthographic' }
     camera: { mode: 'orthographic' }
 }));
 }));

+ 4 - 1
src/tests/browser/render-lines.ts

@@ -15,6 +15,7 @@ import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { Representation } from '../../mol-repr/representation';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -24,7 +25,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 
 
 function linesRepr() {
 function linesRepr() {

+ 4 - 1
src/tests/browser/render-mesh.ts

@@ -17,6 +17,7 @@ import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { Representation } from '../../mol-repr/representation';
 import { Torus } from '../../mol-geo/primitive/torus';
 import { Torus } from '../../mol-geo/primitive/torus';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -26,7 +27,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 
 
 function meshRepr() {
 function meshRepr() {

+ 4 - 1
src/tests/browser/render-shape.ts

@@ -19,6 +19,7 @@ import { Sphere } from '../../mol-geo/primitive/sphere';
 import { ColorNames } from '../../mol-util/color/names';
 import { ColorNames } from '../../mol-util/color/names';
 import { Shape } from '../../mol-model/shape';
 import { Shape } from '../../mol-model/shape';
 import { ShapeRepresentation } from '../../mol-repr/shape/representation';
 import { ShapeRepresentation } from '../../mol-repr/shape/representation';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -28,6 +29,8 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
+const assetManager = new AssetManager();
+
 const info = document.createElement('div');
 const info = document.createElement('div');
 info.style.position = 'absolute';
 info.style.position = 'absolute';
 info.style.fontFamily = 'sans-serif';
 info.style.fontFamily = 'sans-serif';
@@ -38,7 +41,7 @@ info.style.color = 'white';
 parent.appendChild(info);
 parent.appendChild(info);
 
 
 let prevReprLoci = Representation.Loci.Empty;
 let prevReprLoci = Representation.Loci.Empty;
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 canvas3d.input.move.subscribe(({ x, y }) => {
 canvas3d.input.move.subscribe(({ x, y }) => {
     const pickingId = canvas3d.identify(x, y)?.id;
     const pickingId = canvas3d.identify(x, y)?.id;

+ 4 - 1
src/tests/browser/render-spheres.ts

@@ -13,6 +13,7 @@ import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { Representation } from '../../mol-repr/representation';
 import { ParamDefinition } from '../../mol-util/param-definition';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -22,7 +23,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 
 
 function spheresRepr() {
 function spheresRepr() {

+ 4 - 2
src/tests/browser/render-structure.ts

@@ -37,7 +37,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 
 
 const info = document.createElement('div');
 const info = document.createElement('div');
@@ -123,7 +125,7 @@ function getMembraneOrientationRepr() {
 }
 }
 
 
 async function init() {
 async function init() {
-    const ctx = { runtime: SyncRuntimeContext, assetManager: new AssetManager() };
+    const ctx = { runtime: SyncRuntimeContext, assetManager };
 
 
     const cif = await downloadFromPdb('3pqr');
     const cif = await downloadFromPdb('3pqr');
     const models = await getModels(cif);
     const models = await getModels(cif);

+ 4 - 1
src/tests/browser/render-text.ts

@@ -15,6 +15,7 @@ import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { resizeCanvas } from '../../mol-canvas3d/util';
 import { resizeCanvas } from '../../mol-canvas3d/util';
+import { AssetManager } from '../../mol-util/assets';
 
 
 const parent = document.getElementById('app')!;
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
 parent.style.width = '100%';
@@ -24,7 +25,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 resizeCanvas(canvas, parent);
 
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.animate();
 
 
 function textRepr() {
 function textRepr() {

+ 6 - 2
webpack.config.common.js

@@ -30,7 +30,11 @@ const sharedConfig = {
                     { loader: 'css-loader', options: { sourceMap: false } },
                     { loader: 'css-loader', options: { sourceMap: false } },
                     { loader: 'sass-loader', options: { sourceMap: false } },
                     { loader: 'sass-loader', options: { sourceMap: false } },
                 ]
                 ]
-            }
+            },
+            {
+                test: /\.(jpg)$/i,
+                type: 'asset/resource',
+            },
         ]
         ]
     },
     },
     plugins: [
     plugins: [
@@ -76,7 +80,7 @@ function createEntry(src, outFolder, outFilename, isNode) {
 function createEntryPoint(name, dir, out, library) {
 function createEntryPoint(name, dir, out, library) {
     return {
     return {
         entry: path.resolve(__dirname, `lib/${dir}/${name}.js`),
         entry: path.resolve(__dirname, `lib/${dir}/${name}.js`),
-        output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd' },
+        output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd', assetModuleFilename: 'images/[hash][ext][query]', 'publicPath': '' },
         ...sharedConfig
         ...sharedConfig
     };
     };
 }
 }

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä