Bläddra i källkod

Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/252

Alexander Rose 3 år sedan
förälder
incheckning
94fd5a97d6
82 ändrade filer med 1130 tillägg och 14456 borttagningar
  1. 2 1
      .eslintrc.json
  2. 2 2
      .github/workflows/lint.yml
  3. 53 13
      CHANGELOG.md
  4. 2 14094
      package-lock.json
  5. 18 18
      package.json
  6. 10 0
      src/apps/viewer/index.html
  7. 8 4
      src/apps/viewer/index.ts
  8. 25 3
      src/mol-canvas3d/camera.ts
  9. 16 18
      src/mol-canvas3d/camera/util.ts
  10. 12 5
      src/mol-canvas3d/canvas3d.ts
  11. 47 5
      src/mol-canvas3d/helper/interaction-events.ts
  12. 1 0
      src/mol-canvas3d/passes/draw.ts
  13. 17 3
      src/mol-canvas3d/passes/pick.ts
  14. 10 2
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  15. 5 2
      src/mol-geo/geometry/image/image.ts
  16. 1 1
      src/mol-geo/geometry/lines/lines.ts
  17. 14 12
      src/mol-geo/geometry/points/points.ts
  18. 6 2
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  19. 1 2
      src/mol-gl/_spec/renderer.spec.ts
  20. 4 4
      src/mol-gl/renderable/points.ts
  21. 1 1
      src/mol-gl/renderable/schema.ts
  22. 12 4
      src/mol-gl/renderer.ts
  23. 2 2
      src/mol-gl/shader/chunks/apply-marker-color.glsl.ts
  24. 1 2
      src/mol-gl/shader/chunks/assign-material-color.glsl.ts
  25. 1 0
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  26. 1 0
      src/mol-gl/shader/direct-volume.frag.ts
  27. 10 7
      src/mol-gl/shader/lines.vert.ts
  28. 11 9
      src/mol-gl/shader/points.frag.ts
  29. 4 3
      src/mol-gl/shader/points.vert.ts
  30. 3 3
      src/mol-gl/shader/text.vert.ts
  31. 2 2
      src/mol-gl/webgl/context.ts
  32. 10 1
      src/mol-io/reader/common/text/column/token.ts
  33. 9 2
      src/mol-math/geometry/primitives/axes3d.ts
  34. 11 3
      src/mol-math/linear-algebra/3d/vec3.ts
  35. 14 3
      src/mol-math/linear-algebra/tensor.ts
  36. 25 1
      src/mol-math/misc.ts
  37. 2 2
      src/mol-model-formats/structure/basic/atomic.ts
  38. 3 2
      src/mol-model-formats/structure/pdb/atom-site.ts
  39. 27 3
      src/mol-model-formats/structure/pdb/entity.ts
  40. 1 1
      src/mol-model-formats/structure/pdb/secondary-structure.ts
  41. 2 1
      src/mol-model-props/computed/helix-orientation/helix-orientation.ts
  42. 3 3
      src/mol-model/loci.ts
  43. 12 0
      src/mol-model/structure/export/categories/misc.ts
  44. 5 2
      src/mol-model/structure/export/mmcif.ts
  45. 39 5
      src/mol-model/structure/structure/carbohydrates/constants.ts
  46. 14 0
      src/mol-model/structure/structure/element/loci.ts
  47. 2 2
      src/mol-model/structure/structure/unit.ts
  48. 7 2
      src/mol-model/structure/structure/unit/bonds.ts
  49. 2 2
      src/mol-model/structure/structure/unit/bonds/intra-compute.ts
  50. 12 10
      src/mol-plugin-state/manager/interactivity.ts
  51. 48 10
      src/mol-plugin-state/manager/structure/measurement.ts
  52. 4 9
      src/mol-plugin-state/manager/structure/selection.ts
  53. 7 3
      src/mol-plugin-state/transforms/helpers.ts
  54. 2 1
      src/mol-plugin-state/transforms/model.ts
  55. 36 2
      src/mol-plugin-state/transforms/representation.ts
  56. 2 2
      src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx
  57. 5 1
      src/mol-plugin-ui/sequence/sequence.tsx
  58. 30 7
      src/mol-plugin-ui/structure/measurements.tsx
  59. 26 10
      src/mol-plugin/behavior/dynamic/representation.ts
  60. 1 1
      src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts
  61. 14 0
      src/mol-plugin/config.ts
  62. 4 1
      src/mol-plugin/context.ts
  63. 9 4
      src/mol-repr/representation.ts
  64. 45 57
      src/mol-repr/shape/loci/orientation.ts
  65. 84 0
      src/mol-repr/shape/loci/plane.ts
  66. 14 5
      src/mol-repr/structure/representation/line.ts
  67. 2 2
      src/mol-repr/structure/representation/point.ts
  68. 20 7
      src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts
  69. 14 6
      src/mol-repr/structure/visual/bond-inter-unit-line.ts
  70. 20 5
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  71. 13 4
      src/mol-repr/structure/visual/bond-intra-unit-line.ts
  72. 94 0
      src/mol-repr/structure/visual/element-cross.ts
  73. 1 1
      src/mol-repr/structure/visual/element-point.ts
  74. 2 1
      src/mol-repr/structure/visual/util/bond.ts
  75. 96 32
      src/mol-repr/structure/visual/util/link.ts
  76. 8 0
      src/mol-theme/label.ts
  77. 10 2
      src/mol-util/marker-action.ts
  78. 1 1
      src/mol-util/zip/bin.ts
  79. 3 0
      src/servers/model/CHANGELOG.md
  80. 5 5
      src/servers/model/config.ts
  81. 2 2
      src/servers/model/version.ts
  82. 6 6
      src/servers/volume/config.ts

+ 2 - 1
.eslintrc.json

@@ -54,7 +54,8 @@
         "no-multi-spaces": "error",
         "no-multi-spaces": "error",
         "block-spacing": "error",
         "block-spacing": "error",
         "keyword-spacing": "off",
         "keyword-spacing": "off",
-        "space-before-blocks": "error"
+        "space-before-blocks": "error",
+        "semi-spacing": "error"
     },
     },
     "overrides": [
     "overrides": [
         {
         {

+ 2 - 2
.github/workflows/lint.yml

@@ -8,10 +8,10 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
     - uses: actions/checkout@v1
     - uses: actions/checkout@v1
-    - name: install node v12
+    - name: install node v14
       uses: actions/setup-node@v1
       uses: actions/setup-node@v1
       with:
       with:
-        node-version: 12
+        node-version: 14
     - name: yarn install
     - name: yarn install
       run: yarn install
       run: yarn install
     - name: eslint
     - name: eslint

+ 53 - 13
CHANGELOG.md

@@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file, following t
 Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
 Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
 
 
 
 
+## [Unreleased]
+
+- Add additional measurement controls: orientation (box, axes, ellipsoid) & plane (best fit)
+- Improve aromatic bond visuals (add ``aromaticScale``, ``aromaticSpacing``, ``aromaticDashCount`` params)
+- [Breaking] Change ``adjustCylinderLength`` default to ``false`` (set to true for focus representation)
+- Fix marker highlight color overriding select color
+
+## [v2.3.5] - 2021-10-19
+
+- Fix sequence viewer for PDB files with COMPND record and multichain entities.
+- Fix index pair bonds order assignment
+
+## [v2.3.4] - 2021-10-12
+
+- Fix pickScale not taken into account in line/point shader
+- Add pixel-scale, pick-scale & pick-padding GET params to Viewer app
+- Fix selecting bonds not adding their atoms in selection manager
+- Add ``preferAtoms`` option to SelectLoci/HighlightLoci behaviors
+- Make the implicit atoms of bond visuals pickable
+    - Add ``preferAtomPixelPadding`` to Canvas3dInteractionHelper
+- Add points & crosses visuals to Line representation
+- Add ``pickPadding`` config option (look around in case target pixel is empty)
+- Add ``multipleBonds`` param to bond visuals with options: off, symmetric, offset
+- Fix ``argparse`` config in servers.
+
+## [v2.3.3] - 2021-10-01
+
+- Fix direct volume shader
+
+## [v2.3.2] - 2021-10-01
+
+- Prefer WebGL1 on iOS devices until WebGL2 support has stabilized.
+
+## [v2.3.1] - 2021-09-28
+
+- Add Charmm saccharide names
+- Treat missing occupancy column as occupancy of 1
+- Fix line shader not accounting for aspect ratio
+- [Breaking] Fix point repr & shader
+    - Was unusable with ``wboit``
+    - Replaced ``pointFilledCircle`` & ``pointEdgeBleach`` params by ``pointStyle`` (square, circle, fuzzy)
+    - Set ``pointSizeAttenuation`` to false by default
+    - Set ``sizeTheme`` to ``uniform`` by default
+- Add ``markerPriority`` option to Renderer (useful in combination with edges of marking pass)
+- Add support support for ``chem_comp_bond`` and ``struct_conn`` categories (fixes ModelServer behavior where these categories should have been present)
+- Model and VolumeServer: fix argparse config
+
 ## [v2.3.0] - 2021-09-06
 ## [v2.3.0] - 2021-09-06
 
 
 - Take include/exclude flags into account when displaying aromatic bonds
 - Take include/exclude flags into account when displaying aromatic bonds
@@ -111,29 +158,22 @@ Note that since we don't clearly distinguish between a public and private interf
 - Fixed Measurements UI labels (#166)
 - Fixed Measurements UI labels (#166)
 
 
 ## [v2.0.3] - 2021-04-09
 ## [v2.0.3] - 2021-04-09
-### Added
-- Support for ``ColorTheme.palette`` designed for providing gradient-like coloring.
 
 
-### Changed
+- Add support for ``ColorTheme.palette`` designed for providing gradient-like coloring.
 - [Breaking] The ``zip`` function is now asynchronous and expects a ``RuntimeContext``. Also added ``Zip()`` returning a ``Task``.
 - [Breaking] The ``zip`` function is now asynchronous and expects a ``RuntimeContext``. Also added ``Zip()`` returning a ``Task``.
 - [Breaking] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension.
 - [Breaking] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension.
 
 
 ## [v2.0.2] - 2021-03-29
 ## [v2.0.2] - 2021-03-29
-### Added
-- ``Canvas3D.getRenderObjects``.
-- [WIP] Animate state interpolating, including model trajectories
 
 
-### Changed
+- Add ``Canvas3D.getRenderObjects``.
+- [WIP] Animate state interpolating, including model trajectories
 - Recognise MSE, SEP, TPO, PTR and PCA as non-standard amino-acids.
 - Recognise MSE, SEP, TPO, PTR and PCA as non-standard amino-acids.
-
-### Fixed
-- VolumeFromDensityServerCif transform label
-
+- Fix VolumeFromDensityServerCif transform label
 
 
 ## [v2.0.1] - 2021-03-23
 ## [v2.0.1] - 2021-03-23
-### Fixed
-- Exclude tsconfig.commonjs.tsbuildinfo from npm bundle
 
 
+- Exclude tsconfig.commonjs.tsbuildinfo from npm bundle
 
 
 ## [v2.0.0] - 2021-03-23
 ## [v2.0.0] - 2021-03-23
+
 Too many changes to list as this is the start of the changelog... Notably, default exports are now forbidden.
 Too many changes to list as this is the start of the changelog... Notably, default exports are now forbidden.

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 2 - 14094
package-lock.json


+ 18 - 18
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "molstar",
   "name": "molstar",
-  "version": "2.3.0",
+  "version": "2.3.5",
   "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": {
@@ -94,33 +94,33 @@
     "@graphql-codegen/typescript": "^2.2.2",
     "@graphql-codegen/typescript": "^2.2.2",
     "@graphql-codegen/typescript-graphql-files-modules": "^2.1.0",
     "@graphql-codegen/typescript-graphql-files-modules": "^2.1.0",
     "@graphql-codegen/typescript-graphql-request": "^4.1.4",
     "@graphql-codegen/typescript-graphql-request": "^4.1.4",
-    "@graphql-codegen/typescript-operations": "^2.1.4",
+    "@graphql-codegen/typescript-operations": "^2.1.6",
     "@types/cors": "^2.8.12",
     "@types/cors": "^2.8.12",
-    "@typescript-eslint/eslint-plugin": "^4.31.0",
-    "@typescript-eslint/parser": "^4.31.0",
+    "@typescript-eslint/eslint-plugin": "^4.32.0",
+    "@typescript-eslint/parser": "^4.32.0",
     "benchmark": "^2.1.4",
     "benchmark": "^2.1.4",
-    "concurrently": "^6.2.1",
-    "cpx2": "^3.0.2",
+    "concurrently": "^6.3.0",
+    "cpx2": "^4.0.0",
     "crypto-browserify": "^3.12.0",
     "crypto-browserify": "^3.12.0",
-    "css-loader": "^6.2.0",
+    "css-loader": "^6.3.0",
     "eslint": "^7.32.0",
     "eslint": "^7.32.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.0.0",
     "fs-extra": "^10.0.0",
-    "graphql": "^15.5.3",
-    "http-server": "^13.0.1",
-    "jest": "^27.1.1",
+    "graphql": "^15.6.0",
+    "http-server": "^13.0.2",
+    "jest": "^27.2.4",
     "mini-css-extract-plugin": "^2.3.0",
     "mini-css-extract-plugin": "^2.3.0",
     "node-sass": "^6.0.1",
     "node-sass": "^6.0.1",
     "path-browserify": "^1.0.1",
     "path-browserify": "^1.0.1",
     "raw-loader": "^4.0.2",
     "raw-loader": "^4.0.2",
     "sass-loader": "^12.1.0",
     "sass-loader": "^12.1.0",
-    "simple-git": "^2.45.1",
+    "simple-git": "^2.46.0",
     "stream-browserify": "^3.0.0",
     "stream-browserify": "^3.0.0",
-    "style-loader": "^3.2.1",
+    "style-loader": "^3.3.0",
     "ts-jest": "^27.0.5",
     "ts-jest": "^27.0.5",
     "typescript": "^4.4.3",
     "typescript": "^4.4.3",
-    "webpack": "^5.52.1",
+    "webpack": "^5.56.0",
     "webpack-cli": "^4.8.0",
     "webpack-cli": "^4.8.0",
     "webpack-version-file-plugin": "^0.4.0"
     "webpack-version-file-plugin": "^0.4.0"
   },
   },
@@ -129,10 +129,10 @@
     "@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/jest": "^27.0.1",
-    "@types/node": "^16.9.1",
+    "@types/jest": "^27.0.2",
+    "@types/node": "^16.10.2",
     "@types/node-fetch": "^2.5.12",
     "@types/node-fetch": "^2.5.12",
-    "@types/react": "^17.0.20",
+    "@types/react": "^17.0.27",
     "@types/react-dom": "^17.0.9",
     "@types/react-dom": "^17.0.9",
     "@types/swagger-ui-dist": "3.30.1",
     "@types/swagger-ui-dist": "3.30.1",
     "argparse": "^2.0.1",
     "argparse": "^2.0.1",
@@ -146,8 +146,8 @@
     "node-fetch": "^2.6.2",
     "node-fetch": "^2.6.2",
     "react": "^17.0.2",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-dom": "^17.0.2",
-    "rxjs": "^7.3.0",
-    "swagger-ui-dist": "^3.52.1",
+    "rxjs": "^7.3.1",
+    "swagger-ui-dist": "^3.52.3",
     "tslib": "^2.3.1",
     "tslib": "^2.3.1",
     "util.promisify": "^1.1.1",
     "util.promisify": "^1.1.1",
     "xhr2": "^0.2.1"
     "xhr2": "^0.2.1"

+ 10 - 0
src/apps/viewer/index.html

@@ -52,12 +52,22 @@
             var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';
             var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';
             var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
             var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
             var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
             var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
+            var mapProvider = getParam('map-provider', '[^&]+').trim().toLowerCase();
+            var pixelScale = getParam('pixel-scale', '[^&]+').trim();
+            var pickScale = getParam('pick-scale', '[^&]+').trim();
+            var pickPadding = getParam('pick-padding', '[^&]+').trim();
             var viewer = new molstar.Viewer('app', {
             var viewer = new molstar.Viewer('app', {
                 layoutShowControls: !hideControls,
                 layoutShowControls: !hideControls,
                 viewportShowExpand: false,
                 viewportShowExpand: false,
                 collapseLeftPanel: collapseLeftPanel,
                 collapseLeftPanel: collapseLeftPanel,
                 pdbProvider: pdbProvider || 'pdbe',
                 pdbProvider: pdbProvider || 'pdbe',
                 emdbProvider: emdbProvider || 'pdbe',
                 emdbProvider: emdbProvider || 'pdbe',
+                volumeStreamingServer: (mapProvider || 'pdbe') === 'rcsb'
+                    ? 'https://maps.rcsb.org'
+                    : 'https://www.ebi.ac.uk/pdbe/densities',
+                pixelScale: parseFloat(pixelScale) || 1,
+                pickScale: parseFloat(pickScale) || 0.25,
+                pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
             });
             });
 
 
             var snapshotId = getParam('snapshot-id', '[^&]+').trim();
             var snapshotId = getParam('snapshot-id', '[^&]+').trim();

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

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -71,9 +71,11 @@ const DefaultViewerOptions = {
     layoutShowLog: true,
     layoutShowLog: true,
     layoutShowLeftPanel: true,
     layoutShowLeftPanel: true,
     collapseLeftPanel: false,
     collapseLeftPanel: false,
-    disableAntialiasing: false,
-    pixelScale: 1,
-    enableWboit: true,
+    disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
+    pixelScale: PluginConfig.General.PixelScale.defaultValue,
+    pickScale: PluginConfig.General.PickScale.defaultValue,
+    pickPadding: PluginConfig.General.PickPadding.defaultValue,
+    enableWboit: PluginConfig.General.EnableWboit.defaultValue,
 
 
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -130,6 +132,8 @@ export class Viewer {
             config: [
             config: [
                 [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
                 [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
                 [PluginConfig.General.PixelScale, o.pixelScale],
                 [PluginConfig.General.PixelScale, o.pixelScale],
+                [PluginConfig.General.PickScale, o.pickScale],
+                [PluginConfig.General.PickPadding, o.pickPadding],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],

+ 25 - 3
src/mol-canvas3d/camera.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -27,6 +27,10 @@ interface ICamera {
     readonly fogNear: number,
     readonly fogNear: number,
 }
 }
 
 
+const tmpPos1 = Vec3();
+const tmpPos2 = Vec3();
+const tmpClip = Vec4();
+
 class Camera implements ICamera {
 class Camera implements ICamera {
     readonly view: Mat4 = Mat4.identity();
     readonly view: Mat4 = Mat4.identity();
     readonly projection: Mat4 = Mat4.identity();
     readonly projection: Mat4 = Mat4.identity();
@@ -155,14 +159,32 @@ class Camera implements ICamera {
         }
         }
     }
     }
 
 
+    /** Transform point into 2D window coordinates. */
     project(out: Vec4, point: Vec3) {
     project(out: Vec4, point: Vec3) {
         return cameraProject(out, point, this.viewport, this.projectionView);
         return cameraProject(out, point, this.viewport, this.projectionView);
     }
     }
 
 
-    unproject(out: Vec3, point: Vec3) {
+    /**
+     * Transform point from screen space to 3D coordinates.
+     * The point must have `x` and `y` set to 2D window coordinates
+     * and `z` between 0 (near) and 1 (far); the optional `w` is not used.
+     */
+    unproject(out: Vec3, point: Vec3 | Vec4) {
         return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
         return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
     }
     }
 
 
+    /** World space pixel size at given `point` */
+    getPixelSize(point: Vec3) {
+        // project -> unproject of `point` does not exactly return the same
+        // to get a sufficiently accurate measure we unproject the original
+        // clip position in addition to the one shifted bey one pixel
+        this.project(tmpClip, point);
+        this.unproject(tmpPos1, tmpClip);
+        tmpClip[0] += 1;
+        this.unproject(tmpPos2, tmpClip);
+        return Vec3.distance(tmpPos1, tmpPos2);
+    }
+
     constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128), props: Partial<{ pixelScale: number }> = {}) {
     constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128), props: Partial<{ pixelScale: number }> = {}) {
         this.viewport = viewport;
         this.viewport = viewport;
         this.pixelScale = props.pixelScale || 1;
         this.pixelScale = props.pixelScale || 1;
@@ -178,7 +200,7 @@ namespace Camera {
     /**
     /**
      * Sets an offseted view in a larger frustum. This is useful for
      * Sets an offseted view in a larger frustum. This is useful for
      * - multi-window or multi-monitor/multi-machine setups
      * - multi-window or multi-monitor/multi-machine setups
-     * - jittering the camera position for
+     * - jittering the camera position for sampling
      */
      */
     export interface ViewOffset {
     export interface ViewOffset {
         enabled: boolean,
         enabled: boolean,

+ 16 - 18
src/mol-canvas3d/camera/util.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -55,14 +55,11 @@ namespace Viewport {
 
 
 //
 //
 
 
-const NEAR_RANGE = 0;
-const FAR_RANGE = 1;
-
 const tmpVec4 = Vec4();
 const tmpVec4 = Vec4();
 
 
 /** Transform point into 2D window coordinates. */
 /** Transform point into 2D window coordinates. */
 export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
 export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
-    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport;
+    const { x, y, width, height } = viewport;
 
 
     // clip space -> NDC -> window coordinates, implicit 1.0 for w component
     // clip space -> NDC -> window coordinates, implicit 1.0 for w component
     Vec4.set(tmpVec4, point[0], point[1], point[2], 1.0);
     Vec4.set(tmpVec4, point[0], point[1], point[2], 1.0);
@@ -78,27 +75,28 @@ export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projec
         tmpVec4[2] /= w;
         tmpVec4[2] /= w;
     }
     }
 
 
-    // transform into window coordinates, set fourth component is (1/clip.w) as in gl_FragCoord.w
-    out[0] = vX + vWidth / 2 * tmpVec4[0] + (0 + vWidth / 2);
-    out[1] = vY + vHeight / 2 * tmpVec4[1] + (0 + vHeight / 2);
-    out[2] = (FAR_RANGE - NEAR_RANGE) / 2 * tmpVec4[2] + (FAR_RANGE + NEAR_RANGE) / 2;
+    // transform into window coordinates, set fourth component to 1 / clip.w as in gl_FragCoord.w
+    out[0] = (tmpVec4[0] + 1) * width * 0.5 + x;
+    out[1] = (1 - tmpVec4[1]) * height * 0.5 + y; // flip Y
+    out[2] = (tmpVec4[2] + 1) * 0.5;
     out[3] = w === 0 ? 0 : 1 / w;
     out[3] = w === 0 ? 0 : 1 / w;
     return out;
     return out;
 }
 }
 
 
 /**
 /**
  * Transform point from screen space to 3D coordinates.
  * Transform point from screen space to 3D coordinates.
- * The point must have x and y set to 2D window coordinates and z between 0 (near) and 1 (far).
+ * The point must have `x` and `y` set to 2D window coordinates
+ * and `z` between 0 (near) and 1 (far); the optional `w` is not used.
  */
  */
-export function cameraUnproject(out: Vec3, point: Vec3, viewport: Viewport, inverseProjectionView: Mat4) {
-    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport;
+export function cameraUnproject(out: Vec3, point: Vec3 | Vec4, viewport: Viewport, inverseProjectionView: Mat4) {
+    const { x, y, width, height } = viewport;
 
 
-    const x = point[0] - vX;
-    const y = (vHeight - point[1] - 1) - vY;
-    const z = point[2];
+    const px = point[0] - x;
+    const py = (height - point[1] - 1) - y;
+    const pz = point[2];
 
 
-    out[0] = (2 * x) / vWidth - 1;
-    out[1] = (2 * y) / vHeight - 1;
-    out[2] = 2 * z - 1;
+    out[0] = (2 * px) / width - 1;
+    out[1] = (2 * py) / height - 1;
+    out[2] = 2 * pz - 1;
     return Vec3.transformMat4(out, out, inverseProjectionView);
     return Vec3.transformMat4(out, out, inverseProjectionView);
 }
 }

+ 12 - 5
src/mol-canvas3d/canvas3d.ts

@@ -23,7 +23,7 @@ import { Camera } from './camera';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { DebugHelperParams } from './helper/bounding-sphere-helper';
 import { DebugHelperParams } from './helper/bounding-sphere-helper';
 import { SetUtils } from '../mol-util/set';
 import { SetUtils } from '../mol-util/set';
-import { Canvas3dInteractionHelper } from './helper/interaction-events';
+import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
 import { PostprocessingParams } from './passes/postprocessing';
 import { PostprocessingParams } from './passes/postprocessing';
 import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
 import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
 import { PickData } from './passes/pick';
 import { PickData } from './passes/pick';
@@ -84,6 +84,7 @@ export const Canvas3DParams = {
     marking: PD.Group(MarkingParams),
     marking: PD.Group(MarkingParams),
     renderer: PD.Group(RendererParams),
     renderer: PD.Group(RendererParams),
     trackball: PD.Group(TrackballControlsParams),
     trackball: PD.Group(TrackballControlsParams),
+    interaction: PD.Group(Canvas3dInteractionHelperParams),
     debug: PD.Group(DebugHelperParams),
     debug: PD.Group(DebugHelperParams),
     handle: PD.Group(HandleHelperParams),
     handle: PD.Group(HandleHelperParams),
 };
 };
@@ -115,19 +116,23 @@ namespace Canvas3DContext {
         preserveDrawingBuffer: true,
         preserveDrawingBuffer: true,
         pixelScale: 1,
         pixelScale: 1,
         pickScale: 0.25,
         pickScale: 0.25,
-        enableWboit: true
+        /** extra pixels to around target to check in case target is empty */
+        pickPadding: 1,
+        enableWboit: true,
+        preferWebGl1: false
     };
     };
     export type Attribs = typeof DefaultAttribs
     export type Attribs = typeof DefaultAttribs
 
 
     export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
     export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
         const a = { ...DefaultAttribs, ...attribs };
         const a = { ...DefaultAttribs, ...attribs };
-        const { antialias, preserveDrawingBuffer, pixelScale } = a;
+        const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const gl = getGLContext(canvas, {
         const gl = getGLContext(canvas, {
             antialias,
             antialias,
             preserveDrawingBuffer,
             preserveDrawingBuffer,
             alpha: true, // the renderer requires an alpha channel
             alpha: true, // the renderer requires an alpha channel
             depth: true, // the renderer requires a depth buffer
             depth: true, // the renderer requires a depth buffer
             premultipliedAlpha: true, // the renderer outputs PMA
             premultipliedAlpha: true, // the renderer outputs PMA
+            preferWebGl1
         });
         });
         if (gl === null) throw new Error('Could not create a WebGL rendering context');
         if (gl === null) throw new Error('Could not create a WebGL rendering context');
 
 
@@ -305,8 +310,8 @@ namespace Canvas3D {
         const renderer = Renderer.create(webgl, p.renderer);
         const renderer = Renderer.create(webgl, p.renderer);
         const helper = new Helper(webgl, scene, p);
         const helper = new Helper(webgl, scene, p);
 
 
-        const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height });
-        const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
+        const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
+        const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
         const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
         const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
 
 
         let cameraResetRequested = false;
         let cameraResetRequested = false;
@@ -642,6 +647,7 @@ namespace Canvas3D {
                 multiSample: { ...p.multiSample },
                 multiSample: { ...p.multiSample },
                 renderer: { ...renderer.props },
                 renderer: { ...renderer.props },
                 trackball: { ...controls.props },
                 trackball: { ...controls.props },
+                interaction: { ...interactionHelper.props },
                 debug: { ...helper.debug.props },
                 debug: { ...helper.debug.props },
                 handle: { ...helper.handle.props },
                 handle: { ...helper.handle.props },
             };
             };
@@ -778,6 +784,7 @@ namespace Canvas3D {
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.renderer) renderer.setProps(props.renderer);
                 if (props.renderer) renderer.setProps(props.renderer);
                 if (props.trackball) controls.setProps(props.trackball);
                 if (props.trackball) controls.setProps(props.trackball);
+                if (props.interaction) interactionHelper.setProps(props.interaction);
                 if (props.debug) helper.debug.setProps(props.debug);
                 if (props.debug) helper.debug.setProps(props.debug);
                 if (props.handle) helper.handle.setProps(props.handle);
                 if (props.handle) helper.handle.setProps(props.handle);
 
 

+ 47 - 5
src/mol-canvas3d/helper/interaction-events.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -11,6 +11,8 @@ import { InputObserver, ModifiersKeys, ButtonsType } from '../../mol-util/input/
 import { RxEventHelper } from '../../mol-util/rx-event-helper';
 import { RxEventHelper } from '../../mol-util/rx-event-helper';
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Camera } from '../camera';
 import { Camera } from '../camera';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Bond } from '../../mol-model/structure';
 
 
 type Canvas3D = import('../canvas3d').Canvas3D
 type Canvas3D = import('../canvas3d').Canvas3D
 type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
 type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
@@ -19,6 +21,17 @@ type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
 
 
 const enum InputEvent { Move, Click, Drag }
 const enum InputEvent { Move, Click, Drag }
 
 
+const tmpPosA = Vec3();
+const tmpPos = Vec3();
+const tmpNorm = Vec3();
+
+export const Canvas3dInteractionHelperParams = {
+    maxFps: PD.Numeric(30, { min: 10, max: 60, step: 10 }),
+    preferAtomPixelPadding: PD.Numeric(3, { min: 0, max: 20, step: 1 }, { description: 'Number of extra pixels at which to prefer atoms over bonds.' }),
+};
+export type Canvas3dInteractionHelperParams = typeof Canvas3dInteractionHelperParams
+export type Canvas3dInteractionHelperProps = PD.Values<Canvas3dInteractionHelperParams>
+
 export class Canvas3dInteractionHelper {
 export class Canvas3dInteractionHelper {
     private ev = RxEventHelper.create();
     private ev = RxEventHelper.create();
 
 
@@ -48,6 +61,12 @@ export class Canvas3dInteractionHelper {
     private button: ButtonsType.Flag = ButtonsType.create(0);
     private button: ButtonsType.Flag = ButtonsType.create(0);
     private modifiers: ModifiersKeys = ModifiersKeys.None;
     private modifiers: ModifiersKeys = ModifiersKeys.None;
 
 
+    readonly props: Canvas3dInteractionHelperProps;
+
+    setProps(props: Partial<Canvas3dInteractionHelperProps>) {
+        Object.assign(this.props, props);
+    }
+
     private identify(e: InputEvent, t: number) {
     private identify(e: InputEvent, t: number) {
         const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
         const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
 
 
@@ -70,7 +89,7 @@ export class Canvas3dInteractionHelper {
         }
         }
 
 
         if (e === InputEvent.Click) {
         if (e === InputEvent.Click) {
-            const loci = this.getLoci(this.id);
+            const loci = this.getLoci(this.id, this.position);
             this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
             this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
             this.prevLoci = loci;
             this.prevLoci = loci;
             return;
             return;
@@ -78,13 +97,13 @@ export class Canvas3dInteractionHelper {
 
 
         if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
         if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
 
 
-        const loci = this.getLoci(this.id);
+        const loci = this.getLoci(this.id, this.position);
         this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
         this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
         this.prevLoci = loci;
         this.prevLoci = loci;
     }
     }
 
 
     tick(t: number) {
     tick(t: number) {
-        if (this.inside && t - this.prevT > 1000 / this.maxFps) {
+        if (this.inside && t - this.prevT > 1000 / this.props.maxFps) {
             this.prevT = t;
             this.prevT = t;
             this.currentIdentifyT = t;
             this.currentIdentifyT = t;
             this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t);
             this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t);
@@ -144,11 +163,34 @@ export class Canvas3dInteractionHelper {
         );
         );
     }
     }
 
 
+    private getLoci(pickingId: PickingId | undefined, position: Vec3 | undefined) {
+        const { repr, loci } = this.lociGetter(pickingId);
+        if (position && repr && Bond.isLoci(loci) && loci.bonds.length === 2) {
+            const { aUnit, aIndex } = loci.bonds[0];
+            aUnit.conformation.position(aUnit.elements[aIndex], tmpPosA);
+            Vec3.sub(tmpNorm, this.camera.state.position, this.camera.state.target);
+            Vec3.projectPointOnPlane(tmpPos, position, tmpNorm, tmpPosA);
+            const pixelSize = this.camera.getPixelSize(tmpPos);
+            let radius = repr.theme.size.size(loci.bonds[0]) * (repr.props.sizeFactor ?? 1);
+            if (repr.props.lineSizeAttenuation === false) {
+                // divide by two to get radius
+                radius *= pixelSize / 2;
+            }
+            radius += this.props.preferAtomPixelPadding * pixelSize;
+            if (Vec3.distance(tmpPos, tmpPosA) < radius) {
+                return { repr, loci: Bond.toFirstStructureElementLoci(loci) };
+            }
+        }
+        return { repr, loci };
+    }
+
     dispose() {
     dispose() {
         this.ev.dispose();
         this.ev.dispose();
     }
     }
 
 
-    constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private maxFps: number = 30) {
+    constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) {
+        this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
+
         input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
         input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
             this.isInteracting = true;
             this.isInteracting = true;
             // console.log('drag');
             // console.log('drag');

+ 1 - 0
src/mol-canvas3d/passes/draw.ts

@@ -362,6 +362,7 @@ export class DrawPass {
     render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) {
     render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) {
         renderer.setTransparentBackground(transparentBackground);
         renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
+        renderer.setPixelRatio(this.webgl.pixelRatio);
 
 
         if (StereoCamera.is(camera)) {
         if (StereoCamera.is(camera)) {
             this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
             this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);

+ 17 - 3
src/mol-canvas3d/passes/pick.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
  */
  */
@@ -11,6 +11,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item';
 import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Vec3 } from '../../mol-math/linear-algebra';
 import { Vec3 } from '../../mol-math/linear-algebra';
+import { spiral2d } from '../../mol-math/misc';
 import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing';
 import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing';
 import { Camera, ICamera } from '../camera';
 import { Camera, ICamera } from '../camera';
 import { StereoCamera } from '../camera/stereo';
 import { StereoCamera } from '../camera/stereo';
@@ -88,6 +89,7 @@ export class PickPass {
 
 
         this.groupPickTarget.bind();
         this.groupPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'pickGroup');
         this.renderVariant(renderer, camera, scene, helper, 'pickGroup');
+        // printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' })
 
 
         this.depthPickTarget.bind();
         this.depthPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'depth');
         this.renderVariant(renderer, camera, scene, helper, 'depth');
@@ -111,6 +113,8 @@ export class PickHelper {
     private pickHeight: number
     private pickHeight: number
     private halfPickWidth: number
     private halfPickWidth: number
 
 
+    private spiral: [number, number][]
+
     private setupBuffers() {
     private setupBuffers() {
         const bufferSize = this.pickWidth * this.pickHeight * 4;
         const bufferSize = this.pickWidth * this.pickHeight * 4;
         if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
         if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
@@ -138,6 +142,8 @@ export class PickHelper {
 
 
             this.setupBuffers();
             this.setupBuffers();
         }
         }
+
+        this.spiral = spiral2d(Math.round(this.pickScale * this.pickPadding));
     }
     }
 
 
     private syncBuffers() {
     private syncBuffers() {
@@ -177,6 +183,7 @@ export class PickHelper {
 
 
         renderer.setTransparentBackground(false);
         renderer.setTransparentBackground(false);
         renderer.setDrawingBufferSize(this.pickPass.objectPickTarget.getWidth(), this.pickPass.objectPickTarget.getHeight());
         renderer.setDrawingBufferSize(this.pickPass.objectPickTarget.getWidth(), this.pickPass.objectPickTarget.getHeight());
+        renderer.setPixelRatio(this.pickScale);
 
 
         if (StereoCamera.is(camera)) {
         if (StereoCamera.is(camera)) {
             renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
             renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
@@ -192,7 +199,7 @@ export class PickHelper {
         this.dirty = false;
         this.dirty = false;
     }
     }
 
 
-    identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
+    private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
         const { webgl, pickScale } = this;
         const { webgl, pickScale } = this;
         if (webgl.isContextLost) return;
         if (webgl.isContextLost) return;
 
 
@@ -251,7 +258,14 @@ export class PickHelper {
         return { id: { objectId, instanceId, groupId }, position };
         return { id: { objectId, instanceId, groupId }, position };
     }
     }
 
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport) {
+    identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
+        for (const d of this.spiral) {
+            const pickData = this.identifyInternal(x + d[0], y + d[1], camera);
+            if (pickData) return pickData;
+        }
+    }
+
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, readonly pickPadding = 1) {
         this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
         this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
     }
     }
 }
 }

+ 10 - 2
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -9,7 +9,7 @@ import { LocationIterator, PositionLocation } from '../../../mol-geo/util/locati
 import { RenderableState } from '../../../mol-gl/renderable';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume';
 import { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
-import { Texture } from '../../../mol-gl/webgl/texture';
+import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture';
 import { Box3D, Sphere3D } from '../../../mol-math/geometry';
 import { Box3D, Sphere3D } from '../../../mol-math/geometry';
 import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
 import { Theme } from '../../../mol-theme/theme';
@@ -129,7 +129,15 @@ export namespace DirectVolume {
     }
     }
 
 
     export function createEmpty(directVolume?: DirectVolume): DirectVolume {
     export function createEmpty(directVolume?: DirectVolume): DirectVolume {
-        return {} as DirectVolume; // TODO
+        const bbox = Box3D();
+        const gridDimension = Vec3();
+        const transform = Mat4.identity();
+        const unitToCartn = Mat4.identity();
+        const cellDim = Vec3();
+        const texture = createNullTexture();
+        const stats = Grid.One.stats;
+        const packedGroup = false;
+        return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, directVolume);
     }
     }
 
 
     export function createRenderModeParam(stats?: Grid['stats']) {
     export function createRenderModeParam(stats?: Grid['stats']) {

+ 5 - 2
src/mol-geo/geometry/image/image.ts

@@ -7,7 +7,7 @@
 import { hashFnv32a } from '../../../mol-data/util';
 import { hashFnv32a } from '../../../mol-data/util';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { RenderableState } from '../../../mol-gl/renderable';
-import { calculateTransformBoundingSphere, TextureImage } from '../../../mol-gl/renderable/util';
+import { calculateTransformBoundingSphere, createTextureImage, TextureImage } from '../../../mol-gl/renderable/util';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
 import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
 import { Theme } from '../../../mol-theme/theme';
@@ -113,7 +113,10 @@ namespace Image {
     }
     }
 
 
     export function createEmpty(image?: Image): Image {
     export function createEmpty(image?: Image): Image {
-        return {} as Image; // TODO
+        const imageTexture = createTextureImage(0, 4, Uint8Array);
+        const corners = image ? image.cornerBuffer.ref.value : new Float32Array(8 * 3);
+        const groupTexture = createTextureImage(0, 4, Uint8Array);
+        return create(imageTexture, corners, groupTexture, image);
     }
     }
 
 
     export const Params = {
     export const Params = {

+ 1 - 1
src/mol-geo/geometry/lines/lines.ts

@@ -164,7 +164,7 @@ export namespace Lines {
 
 
     export const Params = {
     export const Params = {
         ...BaseGeometry.Params,
         ...BaseGeometry.Params,
-        sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
+        sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
         lineSizeAttenuation: PD.Boolean(false),
         lineSizeAttenuation: PD.Boolean(false),
     };
     };
     export type Params = typeof Params
     export type Params = typeof Params

+ 14 - 12
src/mol-geo/geometry/points/points.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -117,12 +117,19 @@ export namespace Points {
 
 
     //
     //
 
 
+    export const StyleTypes = {
+        'square': 'Square',
+        'circle': 'Circle',
+        'fuzzy': 'Fuzzy',
+    };
+    export type StyleTypes = keyof typeof StyleTypes;
+    export const StyleTypeNames = Object.keys(StyleTypes) as StyleTypes[];
+
     export const Params = {
     export const Params = {
         ...BaseGeometry.Params,
         ...BaseGeometry.Params,
-        sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
+        sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
         pointSizeAttenuation: PD.Boolean(false),
         pointSizeAttenuation: PD.Boolean(false),
-        pointFilledCircle: PD.Boolean(false),
-        pointEdgeBleach: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),
+        pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)),
     };
     };
     export type Params = typeof Params
     export type Params = typeof Params
 
 
@@ -189,8 +196,7 @@ export namespace Points {
             ...BaseGeometry.createValues(props, counts),
             ...BaseGeometry.createValues(props, counts),
             uSizeFactor: ValueCell.create(props.sizeFactor),
             uSizeFactor: ValueCell.create(props.sizeFactor),
             dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
             dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
-            dPointFilledCircle: ValueCell.create(props.pointFilledCircle),
-            uPointEdgeBleach: ValueCell.create(props.pointEdgeBleach),
+            dPointStyle: ValueCell.create(props.pointStyle),
         };
         };
     }
     }
 
 
@@ -204,8 +210,7 @@ export namespace Points {
         BaseGeometry.updateValues(values, props);
         BaseGeometry.updateValues(values, props);
         ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
         ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
         ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation);
         ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation);
-        ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle);
-        ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach);
+        ValueCell.updateIfChanged(values.dPointStyle, props.pointStyle);
     }
     }
 
 
     function updateBoundingSphere(values: PointsValues, points: Points) {
     function updateBoundingSphere(values: PointsValues, points: Points) {
@@ -229,10 +234,7 @@ export namespace Points {
 
 
     function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
     function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
         BaseGeometry.updateRenderableState(state, props);
         BaseGeometry.updateRenderableState(state, props);
-        state.opaque = state.opaque && (
-            !props.pointFilledCircle ||
-            (props.pointFilledCircle && props.pointEdgeBleach === 0)
-        );
+        state.opaque = state.opaque && props.pointStyle !== 'fuzzy';
         state.writeDepth = state.opaque;
         state.writeDepth = state.opaque;
     }
     }
 }
 }

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

@@ -19,7 +19,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
 import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
 import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
-import { Texture } from '../../../mol-gl/webgl/texture';
+import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture';
 import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
 import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
 import { createEmptyClipping } from '../clipping-data';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 import { NullLocation } from '../../../mol-model/location';
@@ -97,7 +97,11 @@ export namespace TextureMesh {
     }
     }
 
 
     export function createEmpty(textureMesh?: TextureMesh): TextureMesh {
     export function createEmpty(textureMesh?: TextureMesh): TextureMesh {
-        return {} as TextureMesh; // TODO
+        const vt = textureMesh ? textureMesh.vertexTexture.ref.value : createNullTexture();
+        const gt = textureMesh ? textureMesh.groupTexture.ref.value : createNullTexture();
+        const nt = textureMesh ? textureMesh.normalTexture.ref.value : createNullTexture();
+        const bs = textureMesh ? textureMesh.boundingSphere : Sphere3D();
+        return create(0, 0, vt, gt, nt, bs, textureMesh);
     }
     }
 
 
     export const Params = {
     export const Params = {

+ 1 - 2
src/mol-gl/_spec/renderer.spec.ts

@@ -85,8 +85,7 @@ function createPoints() {
 
 
         uSizeFactor: ValueCell.create(1),
         uSizeFactor: ValueCell.create(1),
         dPointSizeAttenuation: ValueCell.create(true),
         dPointSizeAttenuation: ValueCell.create(true),
-        dPointFilledCircle: ValueCell.create(false),
-        uPointEdgeBleach: ValueCell.create(0.5),
+        dPointStyle: ValueCell.create('square'),
     };
     };
     const state: RenderableState = {
     const state: RenderableState = {
         disposed: false,
         disposed: false,

+ 4 - 4
src/mol-gl/renderable/points.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -7,9 +7,10 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema';
 import { PointsShaderCode } from '../shader-code';
 import { PointsShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 import { ValueCell } from '../../mol-util';
+import { Points } from '../../mol-geo/geometry/points/points';
 
 
 export const PointsSchema = {
 export const PointsSchema = {
     ...BaseSchema,
     ...BaseSchema,
@@ -17,8 +18,7 @@ export const PointsSchema = {
     aGroup: AttributeSpec('float32', 1, 0),
     aGroup: AttributeSpec('float32', 1, 0),
     aPosition: AttributeSpec('float32', 3, 0),
     aPosition: AttributeSpec('float32', 3, 0),
     dPointSizeAttenuation: DefineSpec('boolean'),
     dPointSizeAttenuation: DefineSpec('boolean'),
-    dPointFilledCircle: DefineSpec('boolean'),
-    uPointEdgeBleach: UniformSpec('f'),
+    dPointStyle: DefineSpec('string', Points.StyleTypeNames),
 };
 };
 export type PointsSchema = typeof PointsSchema
 export type PointsSchema = typeof PointsSchema
 export type PointsValues = Values<PointsSchema>
 export type PointsValues = Values<PointsSchema>

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

@@ -121,7 +121,6 @@ export const GlobalUniformSchema = {
 
 
     uIsOrtho: UniformSpec('f'),
     uIsOrtho: UniformSpec('f'),
     uPixelRatio: UniformSpec('f'),
     uPixelRatio: UniformSpec('f'),
-    uViewportHeight: UniformSpec('f'),
     uViewport: UniformSpec('v4'),
     uViewport: UniformSpec('v4'),
     uViewOffset: UniformSpec('v2'),
     uViewOffset: UniformSpec('v2'),
     uDrawingBufferSize: UniformSpec('v2'),
     uDrawingBufferSize: UniformSpec('v2'),
@@ -162,6 +161,7 @@ export const GlobalUniformSchema = {
     uSelectColor: UniformSpec('v3'),
     uSelectColor: UniformSpec('v3'),
     uHighlightStrength: UniformSpec('f'),
     uHighlightStrength: UniformSpec('f'),
     uSelectStrength: UniformSpec('f'),
     uSelectStrength: UniformSpec('f'),
+    uMarkerPriority: UniformSpec('i'),
 
 
     uXrayEdgeFalloff: UniformSpec('f'),
     uXrayEdgeFalloff: UniformSpec('f'),
 
 

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

@@ -62,6 +62,7 @@ interface Renderer {
     setViewport: (x: number, y: number, width: number, height: number) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
     setTransparentBackground: (value: boolean) => void
     setTransparentBackground: (value: boolean) => void
     setDrawingBufferSize: (width: number, height: number) => void
     setDrawingBufferSize: (width: number, height: number) => void
+    setPixelRatio: (value: number) => void
 
 
     dispose: () => void
     dispose: () => void
 }
 }
@@ -80,6 +81,7 @@ export const RendererParams = {
     selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
     selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
     highlightStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
     highlightStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
     selectStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
     selectStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
+    markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]),
 
 
     xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
     xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
 
 
@@ -233,7 +235,6 @@ namespace Renderer {
             uViewOffset: ValueCell.create(viewOffset),
             uViewOffset: ValueCell.create(viewOffset),
 
 
             uPixelRatio: ValueCell.create(ctx.pixelRatio),
             uPixelRatio: ValueCell.create(ctx.pixelRatio),
-            uViewportHeight: ValueCell.create(viewport.height),
             uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
             uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)),
             uDrawingBufferSize: ValueCell.create(drawingBufferSize),
             uDrawingBufferSize: ValueCell.create(drawingBufferSize),
 
 
@@ -274,6 +275,7 @@ namespace Renderer {
             uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)),
             uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)),
             uHighlightStrength: ValueCell.create(p.highlightStrength),
             uHighlightStrength: ValueCell.create(p.highlightStrength),
             uSelectStrength: ValueCell.create(p.selectStrength),
             uSelectStrength: ValueCell.create(p.selectStrength),
+            uMarkerPriority: ValueCell.create(p.markerPriority),
 
 
             uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
             uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
         };
         };
@@ -571,7 +573,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 !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && !r.values.dPointFilledCircle?.ref.value && !r.values.dXrayShaded?.ref.value) {
+                if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && r.values.dPointStyle?.ref.value !== 'fuzzy' && !r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit');
                     renderObject(r, 'colorWboit');
                 }
                 }
             }
             }
@@ -587,7 +589,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.dRenderMode?.ref.value === 'volume' || r.values.dPointFilledCircle?.ref.value || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit');
                     renderObject(r, 'colorWboit');
                 }
                 }
             }
             }
@@ -670,6 +672,10 @@ namespace Renderer {
                     p.selectStrength = props.selectStrength;
                     p.selectStrength = props.selectStrength;
                     ValueCell.update(globalUniforms.uSelectStrength, p.selectStrength);
                     ValueCell.update(globalUniforms.uSelectStrength, p.selectStrength);
                 }
                 }
+                if (props.markerPriority !== undefined && props.markerPriority !== p.markerPriority) {
+                    p.markerPriority = props.markerPriority;
+                    ValueCell.update(globalUniforms.uMarkerPriority, p.markerPriority);
+                }
 
 
                 if (props.xrayEdgeFalloff !== undefined && props.xrayEdgeFalloff !== p.xrayEdgeFalloff) {
                 if (props.xrayEdgeFalloff !== undefined && props.xrayEdgeFalloff !== p.xrayEdgeFalloff) {
                     p.xrayEdgeFalloff = props.xrayEdgeFalloff;
                     p.xrayEdgeFalloff = props.xrayEdgeFalloff;
@@ -700,7 +706,6 @@ namespace Renderer {
                 gl.scissor(x, y, width, height);
                 gl.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.uViewportHeight, 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));
                 }
                 }
             },
             },
@@ -712,6 +717,9 @@ namespace Renderer {
                     ValueCell.update(globalUniforms.uDrawingBufferSize, Vec2.set(drawingBufferSize, width, height));
                     ValueCell.update(globalUniforms.uDrawingBufferSize, Vec2.set(drawingBufferSize, width, height));
                 }
                 }
             },
             },
+            setPixelRatio: (value: number) => {
+                ValueCell.update(globalUniforms.uPixelRatio, value);
+            },
 
 
             props: p,
             props: p,
             get stats(): RendererStats {
             get stats(): RendererStats {

+ 2 - 2
src/mol-gl/shader/chunks/apply-marker-color.glsl.ts

@@ -1,6 +1,6 @@
 export const apply_marker_color = `
 export const apply_marker_color = `
-if (marker > 0.1) {
-    if (intMod(marker, 2.0) > 0.1) {
+if (marker > 0.0) {
+    if ((uMarkerPriority == 1 && marker != 2.0) || (uMarkerPriority != 1 && marker == 1.0)) {
         gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength);
         gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength);
         gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering
         gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering
     } else {
     } else {

+ 1 - 2
src/mol-gl/shader/chunks/assign-material-color.glsl.ts

@@ -3,9 +3,8 @@ export const assign_material_color = `
     #if defined(dMarkerType_uniform)
     #if defined(dMarkerType_uniform)
         float marker = uMarker;
         float marker = uMarker;
     #elif defined(dMarkerType_groupInstance)
     #elif defined(dMarkerType_groupInstance)
-        float marker = vMarker;
+        float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
     #endif
     #endif
-    marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
 #endif
 #endif
 
 
 #if defined(dRenderVariant_color)
 #if defined(dRenderVariant_color)

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

@@ -23,6 +23,7 @@ uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
 uniform vec3 uSelectColor;
 uniform float uHighlightStrength;
 uniform float uHighlightStrength;
 uniform float uSelectStrength;
 uniform float uSelectStrength;
+uniform int uMarkerPriority;
 
 
 #if defined(dMarkerType_uniform)
 #if defined(dMarkerType_uniform)
     uniform float uMarker;
     uniform float uMarker;

+ 1 - 0
src/mol-gl/shader/direct-volume.frag.ts

@@ -55,6 +55,7 @@ uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
 uniform vec3 uSelectColor;
 uniform float uHighlightStrength;
 uniform float uHighlightStrength;
 uniform float uSelectStrength;
 uniform float uSelectStrength;
+uniform int uMarkerPriority;
 
 
 #if defined(dMarkerType_uniform)
 #if defined(dMarkerType_uniform)
     uniform float uMarker;
     uniform float uMarker;

+ 10 - 7
src/mol-gl/shader/lines.vert.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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,7 +18,7 @@ precision highp int;
 #include common_clip
 #include common_clip
 
 
 uniform float uPixelRatio;
 uniform float uPixelRatio;
-uniform float uViewportHeight;
+uniform vec4 uViewport;
 
 
 attribute mat4 aTransform;
 attribute mat4 aTransform;
 attribute float aInstance;
 attribute float aInstance;
@@ -39,6 +39,8 @@ void trimSegment(const in vec4 start, inout vec4 end) {
 }
 }
 
 
 void main(){
 void main(){
+    float aspect = uViewport.z / uViewport.w;
+
     #include assign_group
     #include assign_group
     #include assign_color_varying
     #include assign_color_varying
     #include assign_marker_varying
     #include assign_marker_varying
@@ -83,15 +85,15 @@ void main(){
     vec2 dir = ndcEnd - ndcStart;
     vec2 dir = ndcEnd - ndcStart;
 
 
     // account for clip-space aspect ratio
     // account for clip-space aspect ratio
-    dir.x *= uPixelRatio;
+    dir.x *= aspect;
     dir = normalize(dir);
     dir = normalize(dir);
 
 
     // perpendicular to dir
     // perpendicular to dir
     vec2 offset = vec2(dir.y, - dir.x);
     vec2 offset = vec2(dir.y, - dir.x);
 
 
     // undo aspect ratio adjustment
     // undo aspect ratio adjustment
-    dir.x /= uPixelRatio;
-    offset.x /= uPixelRatio;
+    dir.x /= aspect;
+    offset.x /= aspect;
 
 
     // sign flip
     // sign flip
     if (aMapping.x < 0.0) offset *= -1.0;
     if (aMapping.x < 0.0) offset *= -1.0;
@@ -99,16 +101,17 @@ void main(){
     // calculate linewidth
     // calculate linewidth
     float linewidth;
     float linewidth;
     #ifdef dLineSizeAttenuation
     #ifdef dLineSizeAttenuation
-        linewidth = size * uPixelRatio * ((uViewportHeight / 2.0) / -start.z) * 5.0;
+        linewidth = size * uPixelRatio * ((uViewport.w / 2.0) / -start.z) * 5.0;
     #else
     #else
         linewidth = size * uPixelRatio;
         linewidth = size * uPixelRatio;
     #endif
     #endif
+    linewidth = max(1.0, linewidth);
 
 
     // adjust for linewidth
     // adjust for linewidth
     offset *= linewidth;
     offset *= linewidth;
 
 
     // adjust for clip-space to screen-space conversion
     // adjust for clip-space to screen-space conversion
-    offset /= uViewportHeight;
+    offset /= uViewport.w;
 
 
     // select end
     // select end
     vec4 clip = (aMapping.y < 0.5) ? clipStart : clipEnd;
     vec4 clip = (aMapping.y < 0.5) ? clipStart : clipEnd;

+ 11 - 9
src/mol-gl/shader/points.frag.ts

@@ -13,10 +13,6 @@ precision highp int;
 #include color_frag_params
 #include color_frag_params
 #include common_clip
 #include common_clip
 
 
-#ifdef dPointFilledCircle
-    uniform float uPointEdgeBleach;
-#endif
-
 const vec2 center = vec2(0.5);
 const vec2 center = vec2(0.5);
 const float radius = 0.5;
 const float radius = 0.5;
 
 
@@ -27,6 +23,15 @@ void main(){
     bool interior = false;
     bool interior = false;
     #include assign_material_color
     #include assign_material_color
 
 
+    #if defined(dPointStyle_circle)
+        float dist = distance(gl_PointCoord, center);
+        if (dist > radius) discard;
+    #elif defined(dPointStyle_fuzzy)
+        float dist = distance(gl_PointCoord, center);
+        float fuzzyAlpha = 1.0 - smoothstep(0.0, radius, dist);
+        if (fuzzyAlpha < 0.0001) discard;
+    #endif
+
     #if defined(dRenderVariant_pick)
     #if defined(dRenderVariant_pick)
         #include check_picking_alpha
         #include check_picking_alpha
         gl_FragColor = material;
         gl_FragColor = material;
@@ -37,11 +42,8 @@ void main(){
     #elif defined(dRenderVariant_color)
     #elif defined(dRenderVariant_color)
         gl_FragColor = material;
         gl_FragColor = material;
 
 
-        #ifdef dPointFilledCircle
-            float dist = distance(gl_PointCoord, center);
-            float alpha = 1.0 - smoothstep(radius - uPointEdgeBleach, radius, dist);
-            if (alpha < 0.0001) discard;
-            gl_FragColor.a *= alpha;
+        #if defined(dPointStyle_fuzzy)
+            gl_FragColor.a *= fuzzyAlpha;
         #endif
         #endif
 
 
         #include apply_marker_color
         #include apply_marker_color

+ 4 - 3
src/mol-gl/shader/points.vert.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -16,7 +16,7 @@ precision highp int;
 #include common_clip
 #include common_clip
 
 
 uniform float uPixelRatio;
 uniform float uPixelRatio;
-uniform float uViewportHeight;
+uniform vec4 uViewport;
 
 
 attribute vec3 aPosition;
 attribute vec3 aPosition;
 attribute mat4 aTransform;
 attribute mat4 aTransform;
@@ -32,10 +32,11 @@ void main(){
     #include assign_size
     #include assign_size
 
 
     #ifdef dPointSizeAttenuation
     #ifdef dPointSizeAttenuation
-        gl_PointSize = size * uPixelRatio * ((uViewportHeight / 2.0) / -mvPosition.z) * 5.0;
+        gl_PointSize = size * uPixelRatio * ((uViewport.w / 2.0) / -mvPosition.z) * 5.0;
     #else
     #else
         gl_PointSize = size * uPixelRatio;
         gl_PointSize = size * uPixelRatio;
     #endif
     #endif
+    gl_PointSize = max(1.0, gl_PointSize);
 
 
     gl_Position = uProjection * mvPosition;
     gl_Position = uProjection * mvPosition;
 
 

+ 3 - 3
src/mol-gl/shader/text.vert.ts

@@ -31,7 +31,7 @@ uniform float uOffsetZ;
 
 
 // uniform bool ortho;
 // uniform bool ortho;
 uniform float uPixelRatio;
 uniform float uPixelRatio;
-uniform float uViewportHeight;
+uniform vec4 uViewport;
 
 
 varying vec2 vTexCoord;
 varying vec2 vTexCoord;
 
 
@@ -60,9 +60,9 @@ void main(void){
     // TODO
     // TODO
     // #ifdef FIXED_SIZE
     // #ifdef FIXED_SIZE
     //     if (ortho) {
     //     if (ortho) {
-    //         scale /= pixelRatio * ((uViewportHeight / 2.0) / -uCameraPosition.z) * 0.1;
+    //         scale /= pixelRatio * ((uViewport.w / 2.0) / -uCameraPosition.z) * 0.1;
     //     } else {
     //     } else {
-    //         scale /= pixelRatio * ((uViewportHeight / 2.0) / -mvPosition.z) * 0.1;
+    //         scale /= pixelRatio * ((uViewport.w / 2.0) / -mvPosition.z) * 0.1;
     //     }
     //     }
     // #endif
     // #endif
 
 

+ 2 - 2
src/mol-gl/webgl/context.ts

@@ -18,7 +18,7 @@ import { now } from '../../mol-util/now';
 import { Texture, TextureFilter } from './texture';
 import { Texture, TextureFilter } from './texture';
 import { ComputeRenderable } from '../renderable';
 import { ComputeRenderable } from '../renderable';
 
 
-export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes): GLRenderingContext | null {
+export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes & { preferWebGl1?: boolean }): GLRenderingContext | null {
     function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') {
     function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') {
         try {
         try {
             return canvas.getContext(id, attribs) as GLRenderingContext | null;
             return canvas.getContext(id, attribs) as GLRenderingContext | null;
@@ -26,7 +26,7 @@ export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAt
             return null;
             return null;
         }
         }
     }
     }
-    const gl = get('webgl2') || get('webgl') || get('experimental-webgl');
+    const gl = (attribs?.preferWebGl1 ? null : get('webgl2')) || get('webgl') || get('experimental-webgl');
     if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`);
     if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`);
     return gl;
     return gl;
 }
 }

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

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
  */
  */
 
 
 import { Column, ColumnHelpers } from '../../../../../mol-data/db';
 import { Column, ColumnHelpers } from '../../../../../mol-data/db';
@@ -50,4 +51,12 @@ export function areValuesEqualProvider(tokens: Tokens) {
         }
         }
         return true;
         return true;
     };
     };
+}
+
+export function areTokensEmpty(tokens: Tokens) {
+    const { count, indices } = tokens;
+    for (let i = 0; i < count; ++i) {
+        if (indices[2 * i] !== indices[2 * i + 1]) return false;
+    }
+    return true;
 }
 }

+ 9 - 2
src/mol-math/geometry/primitives/axes3d.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
  */
  */
@@ -48,7 +48,7 @@ namespace Axes3D {
         return out;
         return out;
     }
     }
 
 
-    const tmpTransformMat3 = Mat3.zero();
+    const tmpTransformMat3 = Mat3();
     /** Transform axes with a Mat4 */
     /** Transform axes with a Mat4 */
     export function transform(out: Axes3D, a: Axes3D, m: Mat4): Axes3D {
     export function transform(out: Axes3D, a: Axes3D, m: Mat4): Axes3D {
         Vec3.transformMat4(out.origin, a.origin, m);
         Vec3.transformMat4(out.origin, a.origin, m);
@@ -58,6 +58,13 @@ namespace Axes3D {
         Vec3.transformMat3(out.dirC, a.dirC, n);
         Vec3.transformMat3(out.dirC, a.dirC, n);
         return out;
         return out;
     }
     }
+
+    export function scale(out: Axes3D, a: Axes3D, scale: number): Axes3D {
+        Vec3.scale(out.dirA, a.dirA, scale);
+        Vec3.scale(out.dirB, a.dirB, scale);
+        Vec3.scale(out.dirC, a.dirC, scale);
+        return out;
+    }
 }
 }
 
 
 export { Axes3D };
 export { Axes3D };

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

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
@@ -540,9 +540,17 @@ namespace Vec3 {
 
 
     /** Project `point` onto `vector` starting from `origin` */
     /** Project `point` onto `vector` starting from `origin` */
     export function projectPointOnVector(out: Vec3, point: Vec3, vector: Vec3, origin: Vec3) {
     export function projectPointOnVector(out: Vec3, point: Vec3, vector: Vec3, origin: Vec3) {
-        sub(out, copy(out, point), origin);
+        sub(out, point, origin);
         const scalar = dot(vector, out) / squaredMagnitude(vector);
         const scalar = dot(vector, out) / squaredMagnitude(vector);
-        return add(out, scale(out, copy(out, vector), scalar), origin);
+        return add(out, scale(out, vector, scalar), origin);
+    }
+
+    const tmpProjectPlane = zero();
+    /** Project `point` onto `plane` defined by `normal` starting from `origin` */
+    export function projectPointOnPlane(out: Vec3, point: Vec3, normal: Vec3, origin: Vec3) {
+        normalize(tmpProjectPlane, normal);
+        sub(out, point, origin);
+        return sub(out, point, scale(tmpProjectPlane, tmpProjectPlane, dot(out, tmpProjectPlane)));
     }
     }
 
 
     export function projectOnVector(out: Vec3, p: Vec3, vector: Vec3) {
     export function projectOnVector(out: Vec3, p: Vec3, vector: Vec3) {

+ 14 - 3
src/mol-math/linear-algebra/tensor.ts

@@ -109,7 +109,10 @@ export namespace Tensor {
                 set: (t, d, x) => t[d] = x,
                 set: (t, d, x) => t[d] = x,
                 add: (t, d, x) => t[d] += x,
                 add: (t, d, x) => t[d] += x,
                 dataOffset: (d) => d,
                 dataOffset: (d) => d,
-                getCoords: (o, c) => { c[0] = o; return c as number[]; }
+                getCoords: (o, c) => {
+                    c[0] = o;
+                    return c as number[];
+                }
             };
             };
             case 2: {
             case 2: {
                 // column major
                 // column major
@@ -120,7 +123,11 @@ export namespace Tensor {
                         set: (t, i, j, x) => t[j * rows + i] = x,
                         set: (t, i, j, x) => t[j * rows + i] = x,
                         add: (t, i, j, x) => t[j * rows + i] += x,
                         add: (t, i, j, x) => t[j * rows + i] += x,
                         dataOffset: (i, j) => j * rows + i,
                         dataOffset: (i, j) => j * rows + i,
-                        getCoords: (o, c) => { c[0] = o % rows; c[1] = Math.floor(o / rows) ; return c as number[]; }
+                        getCoords: (o, c) => {
+                            c[0] = o % rows;
+                            c[1] = Math.floor(o / rows);
+                            return c as number[];
+                        }
                     };
                     };
                 }
                 }
                 if (ao[0] === 1 && ao[1] === 0) {
                 if (ao[0] === 1 && ao[1] === 0) {
@@ -130,7 +137,11 @@ export namespace Tensor {
                         set: (t, i, j, x) => t[i * cols + j] = x,
                         set: (t, i, j, x) => t[i * cols + j] = x,
                         add: (t, i, j, x) => t[i * cols + j] += x,
                         add: (t, i, j, x) => t[i * cols + j] += x,
                         dataOffset: (i, j) => i * cols + j,
                         dataOffset: (i, j) => i * cols + j,
-                        getCoords: (o, c) => { c[0] = Math.floor(o / cols); c[1] = o % cols; return c as number[]; }
+                        getCoords: (o, c) => {
+                            c[0] = Math.floor(o / cols);
+                            c[1] = o % cols;
+                            return c as number[];
+                        }
                     };
                     };
                 }
                 }
                 throw new Error('bad axis order');
                 throw new Error('bad axis order');

+ 25 - 1
src/mol-math/misc.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -37,4 +37,28 @@ export function absMax(...values: number[]) {
 /** Length of an arc with angle in radians */
 /** Length of an arc with angle in radians */
 export function arcLength(angle: number, radius: number) {
 export function arcLength(angle: number, radius: number) {
     return angle * radius;
     return angle * radius;
+}
+
+/** Create an outward spiral of given `radius` on a 2d grid */
+export function spiral2d(radius: number) {
+    let x = 0;
+    let y = 0;
+    const delta = [0, -1];
+    const size = radius * 2 + 1;
+    const halfSize = size / 2;
+    const out: [number, number][] = [];
+
+    for (let i = Math.pow(size, 2); i > 0; --i) {
+        if ((-halfSize < x && x <= halfSize) && (-halfSize < y && y <= halfSize)) {
+            out.push([x, y]);
+        }
+
+        if (x === y || (x < 0 && x === -y) || (x > 0 && x === 1 - y)) {
+            [delta[0], delta[1]] = [-delta[1], delta[0]]; // change direction
+        }
+
+        x += delta[0];
+        y += delta[1];
+    }
+    return out;
 }
 }

+ 2 - 2
src/mol-model-formats/structure/basic/atomic.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
@@ -100,7 +100,7 @@ function getConformation(atom_site: AtomSite): AtomicConformation {
     return {
     return {
         id: UUID.create22(),
         id: UUID.create22(),
         atomId: atom_site.id,
         atomId: atom_site.id,
-        occupancy: atom_site.occupancy,
+        occupancy: atom_site.occupancy.isDefined ? atom_site.occupancy : Column.ofConst(1, atom_site._rowCount, Column.Schema.float),
         B_iso_or_equiv: atom_site.B_iso_or_equiv,
         B_iso_or_equiv: atom_site.B_iso_or_equiv,
         xyzDefined: atom_site.Cartn_x.isDefined && atom_site.Cartn_y.isDefined && atom_site.Cartn_z.isDefined,
         xyzDefined: atom_site.Cartn_x.isDefined && atom_site.Cartn_y.isDefined && atom_site.Cartn_z.isDefined,
         x: atom_site.Cartn_x.toArray({ array: Float32Array }),
         x: atom_site.Cartn_x.toArray({ array: Float32Array }),

+ 3 - 2
src/mol-model-formats/structure/pdb/atom-site.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
@@ -10,6 +10,7 @@ import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
 import { TokenBuilder, Tokenizer } from '../../../mol-io/reader/common/text/tokenizer';
 import { TokenBuilder, Tokenizer } from '../../../mol-io/reader/common/text/tokenizer';
 import { guessElementSymbolTokens } from '../util';
 import { guessElementSymbolTokens } from '../util';
 import { Column } from '../../../mol-data/db';
 import { Column } from '../../../mol-data/db';
+import { areTokensEmpty } from '../../../mol-io/reader/common/text/column/token';
 
 
 type AtomSiteTemplate = typeof getAtomSiteTemplate extends (...args: any) => infer T ? T : never
 type AtomSiteTemplate = typeof getAtomSiteTemplate extends (...args: any) => infer T ? T : never
 export function getAtomSiteTemplate(data: string, count: number) {
 export function getAtomSiteTemplate(data: string, count: number) {
@@ -63,7 +64,7 @@ export function getAtomSite(sites: AtomSiteTemplate): { [K in keyof mmCIF_Schema
         label_seq_id: CifField.ofUndefined(sites.index, Column.Schema.int),
         label_seq_id: CifField.ofUndefined(sites.index, Column.Schema.int),
         label_entity_id: CifField.ofStrings(sites.label_entity_id),
         label_entity_id: CifField.ofStrings(sites.label_entity_id),
 
 
-        occupancy: CifField.ofTokens(sites.occupancy),
+        occupancy: areTokensEmpty(sites.occupancy) ? CifField.ofUndefined(sites.index, Column.Schema.float) : CifField.ofTokens(sites.occupancy),
         type_symbol: CifField.ofTokens(sites.type_symbol),
         type_symbol: CifField.ofTokens(sites.type_symbol),
 
 
         pdbx_PDB_ins_code: CifField.ofTokens(sites.pdbx_PDB_ins_code),
         pdbx_PDB_ins_code: CifField.ofTokens(sites.pdbx_PDB_ins_code),

+ 27 - 3
src/mol-model-formats/structure/pdb/entity.ts

@@ -25,7 +25,7 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) {
 
 
     let currentSpec: Spec | undefined;
     let currentSpec: Spec | undefined;
     let currentCompound: EntityCompound = { chains: [], description: '' };
     let currentCompound: EntityCompound = { chains: [], description: '' };
-    const Compounds: EntityCompound[] = [];
+    const compounds: EntityCompound[] = [];
 
 
     for (let i = lineStart; i < lineEnd; i++) {
     for (let i = lineStart; i < lineEnd; i++) {
         const line = getLine(i);
         const line = getLine(i);
@@ -55,7 +55,7 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) {
                 chains: [],
                 chains: [],
                 description: ''
                 description: ''
             };
             };
-            Compounds.push(currentCompound);
+            compounds.push(currentCompound);
         } else if (currentSpec === 'MOLECULE') {
         } else if (currentSpec === 'MOLECULE') {
             if (currentCompound.description) currentCompound.description += ' ';
             if (currentCompound.description) currentCompound.description += ' ';
             currentCompound.description += value;
             currentCompound.description += value;
@@ -64,7 +64,31 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) {
         }
         }
     }
     }
 
 
-    return Compounds;
+    // Define a seprate entity for each chain
+    // --------------------------------------
+    //
+    // This is a workaround for how sequences are currently determined for PDB files.
+    //
+    // The current approach infers the "observed sequence" from the atomic hierarchy.
+    // However, for example for PDB ID 3HHR, this approach fails, since chains B and C
+    // belong to the same entity but contain different observed sequence, which causes display
+    // errors in the sequence viewer (since the sequences are determined "per entity").
+    //
+    // A better approach could be to parse SEQRES categories and use it to construct
+    // entity_poly_seq category. However, this would require constructing label_seq_id (with gaps)
+    // from RES ID pdb column (auth_seq_id), which isn't a trivial exercise.
+    //
+    // (properly formatted) mmCIF structures do not exhibit this issue.
+    const singletons: EntityCompound[] = [];
+    for (const comp of compounds) {
+        for (const chain of comp.chains) {
+            singletons.push({
+                description: comp.description,
+                chains: [chain]
+            });
+        }
+    }
+    return singletons;
 }
 }
 
 
 export function parseHetnam(lines: Tokens, lineStart: number, lineEnd: number) {
 export function parseHetnam(lines: Tokens, lineStart: number, lineEnd: number) {

+ 1 - 1
src/mol-model-formats/structure/pdb/secondary-structure.ts

@@ -102,7 +102,7 @@ export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): C
     const beg_auth_comp_id = CifField.ofStrings(helices.map(h => h.initResName));
     const beg_auth_comp_id = CifField.ofStrings(helices.map(h => h.initResName));
 
 
     const end_auth_asym_id = CifField.ofStrings(helices.map(h => h.endChainID));
     const end_auth_asym_id = CifField.ofStrings(helices.map(h => h.endChainID));
-    const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName));;
+    const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName));
 
 
     const struct_conf: CifCategory.Fields<mmCIF_Schema['struct_conf']> = {
     const struct_conf: CifCategory.Fields<mmCIF_Schema['struct_conf']> = {
         beg_label_asym_id: beg_auth_asym_id,
         beg_label_asym_id: beg_auth_asym_id,

+ 2 - 1
src/mol-model-props/computed/helix-orientation/helix-orientation.ts

@@ -127,7 +127,8 @@ export function calcHelixOrientation(model: Model): HelixOrientation {
         Vec3.fromArray(v2, centers, e3 - 6);
         Vec3.fromArray(v2, centers, e3 - 6);
         Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
         Vec3.normalize(axis, Vec3.sub(axis, v1, v2));
         const eI = traceElementIndex[e];
         const eI = traceElementIndex[e];
-        Vec3.set(a1, x[eI], y[eI], z[eI]);Vec3.copy(vt, a1);
+        Vec3.set(a1, x[eI], y[eI], z[eI]);
+        Vec3.copy(vt, a1);
         Vec3.projectPointOnVector(vt, vt, axis, v1);
         Vec3.projectPointOnVector(vt, vt, axis, v1);
         Vec3.toArray(vt, centers, e3);
         Vec3.toArray(vt, centers, e3);
     }
     }

+ 3 - 3
src/mol-model/loci.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
@@ -283,8 +283,8 @@ namespace Loci {
      * Converts structure related loci to StructureElement.Loci and applies
      * Converts structure related loci to StructureElement.Loci and applies
      * granularity if given
      * granularity if given
      */
      */
-    export function normalize(loci: Loci, granularity?: Granularity) {
-        if (granularity !== 'element' && Bond.isLoci(loci)) {
+    export function normalize(loci: Loci, granularity?: Granularity, alwaysConvertBonds = false) {
+        if ((granularity !== 'element' || alwaysConvertBonds) && Bond.isLoci(loci)) {
             // convert Bond.Loci to a StructureElement.Loci so granularity can be applied
             // convert Bond.Loci to a StructureElement.Loci so granularity can be applied
             loci = Bond.toStructureElementLoci(loci);
             loci = Bond.toStructureElementLoci(loci);
         }
         }

+ 12 - 0
src/mol-model/structure/export/categories/misc.ts

@@ -22,6 +22,18 @@ export const _chem_comp: CifCategory<CifExportContext> = {
     }
     }
 };
 };
 
 
+export const _chem_comp_bond: CifCategory<CifExportContext> = {
+    name: 'chem_comp_bond',
+    instance({ firstModel, structures, cache }) {
+        const chem_comp_bond = getModelMmCifCategory(structures[0].model, 'chem_comp_bond');
+        if (!chem_comp_bond) return CifCategory.Empty;
+        const { comp_id } = chem_comp_bond;
+        const names = cache.uniqueResidueNames || (cache.uniqueResidueNames = getUniqueResidueNamesFromStructures(structures));
+        const indices = Column.indicesOf(comp_id, id => names.has(id));
+        return CifCategory.ofTable(chem_comp_bond, indices);
+    }
+};
+
 export const _pdbx_chem_comp_identifier: CifCategory<CifExportContext> = {
 export const _pdbx_chem_comp_identifier: CifCategory<CifExportContext> = {
     name: 'pdbx_chem_comp_identifier',
     name: 'pdbx_chem_comp_identifier',
     instance({ firstModel, structures, cache }) {
     instance({ firstModel, structures, cache }) {

+ 5 - 2
src/mol-model/structure/export/mmcif.ts

@@ -11,7 +11,7 @@ import { Structure } from '../structure';
 import { _atom_site } from './categories/atom_site';
 import { _atom_site } from './categories/atom_site';
 import CifCategory = CifWriter.Category
 import CifCategory = CifWriter.Category
 import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure';
 import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure';
-import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc';
+import { _chem_comp, _chem_comp_bond, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc';
 import { Model } from '../model';
 import { Model } from '../model';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category, copy_source_mmCifCategory } from './categories/utils';
 import { getUniqueEntityIndicesFromStructures, copy_mmCif_category, copy_source_mmCifCategory } from './categories/utils';
 import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence';
 import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence';
@@ -81,9 +81,12 @@ const Categories = [
     copy_mmCif_category('pdbx_entity_branch_link'),
     copy_mmCif_category('pdbx_entity_branch_link'),
     copy_mmCif_category('pdbx_branch_scheme'),
     copy_mmCif_category('pdbx_branch_scheme'),
 
 
+    // Struct conn
+    copy_mmCif_category('struct_conn'),
+
     // Misc
     // Misc
-    // TODO: filter for actual present residues?
     _chem_comp,
     _chem_comp,
+    _chem_comp_bond,
     _pdbx_chem_comp_identifier,
     _pdbx_chem_comp_identifier,
     copy_mmCif_category('atom_sites'),
     copy_mmCif_category('atom_sites'),
 
 

+ 39 - 5
src/mol-model/structure/structure/carbohydrates/constants.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -309,14 +309,48 @@ const UnknownSaccharideNames = [
     'PUF', 'GDA', '9WJ', // via updated CCD
     'PUF', 'GDA', '9WJ', // via updated CCD
 ];
 ];
 
 
+/**
+ * From http://glycam.org/docs/othertoolsservice/2016/06/09/3d-snfg-list-of-residue-names/#CHARMM
+ */
+const CharmmSaccharideNames: { [k: string]: string[] } = {
+    Glc: ['AGLC', 'BGLC'],
+    GlcNAc: ['AGLCNA', 'BGLCNA', 'BGLCN0'],
+    GlcA: ['AGLCA', 'BGLCA', 'BGLCA0'],
+    Man: ['AMAN', 'BMAN'],
+    Rha: ['ARHM', 'BRHM'],
+    Ara: ['AARB', 'BARB'],
+    Gal: ['AGAL', 'BGAL'],
+    GalNAc: ['AGALNA', 'BGALNA'],
+    Gul: ['AGUL', 'BGUL'],
+    Alt: ['AALT', 'BALT'],
+    All: ['AALL', 'BALL'],
+    Tal: ['ATAL', 'BTAL'],
+    Ido: ['AIDO', 'BIDO'],
+    IdoA: ['AIDOA', 'BIDOA'],
+    Fuc: ['AFUC', 'BFUC'],
+    Lyx: ['ALYF', 'BLYF'],
+    Xyl: ['AXYL', 'BXYL', 'AXYF', 'BXYF'],
+    Rib: ['ARIB', 'BRIB'],
+    Fru: ['AFRU', 'BFRU'],
+    Neu5Ac: ['ANE5AC', 'BNE5AC'],
+};
+
 export const SaccharideCompIdMap = (function () {
 export const SaccharideCompIdMap = (function () {
     const map = new Map<string, SaccharideComponent>();
     const map = new Map<string, SaccharideComponent>();
     for (let i = 0, il = Monosaccharides.length; i < il; ++i) {
     for (let i = 0, il = Monosaccharides.length; i < il; ++i) {
         const saccharide = Monosaccharides[i];
         const saccharide = Monosaccharides[i];
-        const names = CommonSaccharideNames[saccharide.abbr];
-        if (names) {
-            for (let j = 0, jl = names.length; j < jl; ++j) {
-                map.set(names[j], saccharide);
+
+        const common = CommonSaccharideNames[saccharide.abbr];
+        if (common) {
+            for (let j = 0, jl = common.length; j < jl; ++j) {
+                map.set(common[j], saccharide);
+            }
+        }
+
+        const charmm = CharmmSaccharideNames[saccharide.abbr];
+        if (charmm) {
+            for (let j = 0, jl = charmm.length; j < jl; ++j) {
+                map.set(charmm[j], saccharide);
             }
             }
         }
         }
     }
     }

+ 14 - 0
src/mol-model/structure/structure/element/loci.ts

@@ -582,6 +582,20 @@ export namespace Loci {
         return PrincipalAxes.ofPositions(positions);
         return PrincipalAxes.ofPositions(positions);
     }
     }
 
 
+    export function getPrincipalAxesMany(locis: Loci[]): PrincipalAxes {
+        let elementCount = 0;
+        locis.forEach(l => {
+            elementCount += size(l);
+        });
+        const positions = new Float32Array(3 * elementCount);
+        let offset = 0;
+        locis.forEach(l => {
+            toPositionsArray(l, positions, offset);
+            offset += size(l) * 3;
+        });
+        return PrincipalAxes.ofPositions(positions);
+    }
+
     function sourceIndex(unit: Unit, element: ElementIndex) {
     function sourceIndex(unit: Unit, element: ElementIndex) {
         return Unit.isAtomic(unit)
         return Unit.isAtomic(unit)
             ? unit.model.atomicHierarchy.atomSourceIndex.value(element)
             ? unit.model.atomicHierarchy.atomSourceIndex.value(element)

+ 2 - 2
src/mol-model/structure/structure/unit.ts

@@ -369,7 +369,7 @@ namespace Unit {
         readonly props: CoarseProperties;
         readonly props: CoarseProperties;
 
 
         getChild(elements: StructureElement.Set): Unit {
         getChild(elements: StructureElement.Set): Unit {
-            if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */;
+            if (elements.length === this.elements.length) return this as any as Unit; // lets call this an ugly temporary hack
             return createCoarse(this.id, this.invariantId, this.chainGroupId, this.traits, this.model, this.kind, elements, this.conformation, CoarseProperties());
             return createCoarse(this.id, this.invariantId, this.chainGroupId, this.traits, this.model, this.kind, elements, this.conformation, CoarseProperties());
         }
         }
 
 
@@ -465,7 +465,7 @@ namespace Unit {
     export class Gaussians extends Coarse<Kind.Gaussians, CoarseGaussianConformation> { }
     export class Gaussians extends Coarse<Kind.Gaussians, CoarseGaussianConformation> { }
 
 
     function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, chainGroupId: number, traits: Traits, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties): K extends Kind.Spheres ? Spheres : Gaussians {
     function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, chainGroupId: number, traits: Traits, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties): K extends Kind.Spheres ? Spheres : Gaussians {
-        return new Coarse(id, invariantId, chainGroupId, traits, model, kind, elements, conformation, props) as any /** lets call this an ugly temporary hack */;
+        return new Coarse(id, invariantId, chainGroupId, traits, model, kind, elements, conformation, props) as any; // lets call this an ugly temporary hack
     }
     }
 
 
     export function areSameChainOperatorGroup(a: Unit, b: Unit) {
     export function areSameChainOperatorGroup(a: Unit, b: Unit) {

+ 7 - 2
src/mol-model/structure/structure/unit/bonds.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 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>
@@ -8,7 +8,7 @@
 import { Unit, StructureElement } from '../../structure';
 import { Unit, StructureElement } from '../../structure';
 import { Structure } from '../structure';
 import { Structure } from '../structure';
 import { BondType } from '../../model/types';
 import { BondType } from '../../model/types';
-import { SortedArray, Iterator } from '../../../../mol-data/int';
+import { SortedArray, Iterator, OrderedSet } from '../../../../mol-data/int';
 import { CentroidHelper } from '../../../../mol-math/geometry/centroid-helper';
 import { CentroidHelper } from '../../../../mol-math/geometry/centroid-helper';
 import { Sphere3D } from '../../../../mol-math/geometry';
 import { Sphere3D } from '../../../../mol-math/geometry';
 
 
@@ -132,6 +132,11 @@ namespace Bond {
         return StructureElement.Loci(loci.structure, elements);
         return StructureElement.Loci(loci.structure, elements);
     }
     }
 
 
+    export function toFirstStructureElementLoci(loci: Loci): StructureElement.Loci {
+        const { aUnit, aIndex } = loci.bonds[0];
+        return StructureElement.Loci(loci.structure, [{ unit: aUnit, indices: OrderedSet.ofSingleton(aIndex) }]);
+    }
+
     export function getType(structure: Structure, location: Location<Unit.Atomic>): BondType {
     export function getType(structure: Structure, location: Location<Unit.Atomic>): BondType {
         if (location.aUnit === location.bUnit) {
         if (location.aUnit === location.bUnit) {
             const bonds = location.aUnit.bonds;
             const bonds = location.aUnit.bonds;

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

@@ -79,7 +79,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
             if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxDistance) {
             if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxDistance) {
                 atomA[atomA.length] = _aI;
                 atomA[atomA.length] = _aI;
                 atomB[atomB.length] = _bI;
                 atomB[atomB.length] = _bI;
-                orders[order.length] = order[i];
+                orders[orders.length] = order[i];
                 flags[flags.length] = flag[i];
                 flags[flags.length] = flag[i];
             }
             }
         }
         }
@@ -133,7 +133,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
 
 
                 const p = partnerA.atomIndex === aI ? partnerB : partnerA;
                 const p = partnerA.atomIndex === aI ? partnerB : partnerA;
                 const _bI = SortedArray.indexOf(unit.elements, p.atomIndex) as StructureElement.UnitIndex;
                 const _bI = SortedArray.indexOf(unit.elements, p.atomIndex) as StructureElement.UnitIndex;
-                if (_bI < 0) continue;
+                if (_bI < 0 || atoms[_bI] < aI) continue;
 
 
                 atomA[atomA.length] = _aI;
                 atomA[atomA.length] = _aI;
                 atomB[atomB.length] = _bI;
                 atomB[atomB.length] = _bI;

+ 12 - 10
src/mol-plugin-state/manager/interactivity.ts

@@ -101,10 +101,10 @@ namespace InteractivityManager {
             // TODO clear, then re-apply remaining providers
             // TODO clear, then re-apply remaining providers
         }
         }
 
 
-        protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity = true) {
+        protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity: boolean, alwaysConvertBonds = false) {
             const { loci, repr } = reprLoci;
             const { loci, repr } = reprLoci;
             const granularity = applyGranularity ? this.props.granularity : undefined;
             const granularity = applyGranularity ? this.props.granularity : undefined;
-            return { loci: Loci.normalize(loci, granularity), repr };
+            return { loci: Loci.normalize(loci, granularity, alwaysConvertBonds), repr };
         }
         }
 
 
         protected mark(current: Representation.Loci, action: MarkerAction, noRender = false) {
         protected mark(current: Representation.Loci, action: MarkerAction, noRender = false) {
@@ -187,7 +187,8 @@ namespace InteractivityManager {
         toggle(current: Representation.Loci, applyGranularity = true) {
         toggle(current: Representation.Loci, applyGranularity = true) {
             if (Loci.isEmpty(current.loci)) return;
             if (Loci.isEmpty(current.loci)) return;
 
 
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
+
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 this.toggleSel(normalized);
                 this.toggleSel(normalized);
             } else {
             } else {
@@ -198,7 +199,7 @@ namespace InteractivityManager {
         toggleExtend(current: Representation.Loci, applyGranularity = true) {
         toggleExtend(current: Representation.Loci, applyGranularity = true) {
             if (Loci.isEmpty(current.loci)) return;
             if (Loci.isEmpty(current.loci)) return;
 
 
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
                 const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci;
                 this.toggleSel({ loci, repr: normalized.repr });
                 this.toggleSel({ loci, repr: normalized.repr });
@@ -206,7 +207,7 @@ namespace InteractivityManager {
         }
         }
 
 
         select(current: Representation.Loci, applyGranularity = true) {
         select(current: Representation.Loci, applyGranularity = true) {
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 this.sel.modify('add', normalized.loci);
                 this.sel.modify('add', normalized.loci);
             }
             }
@@ -214,7 +215,7 @@ namespace InteractivityManager {
         }
         }
 
 
         selectJoin(current: Representation.Loci, applyGranularity = true) {
         selectJoin(current: Representation.Loci, applyGranularity = true) {
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 this.sel.modify('intersect', normalized.loci);
                 this.sel.modify('intersect', normalized.loci);
             }
             }
@@ -222,7 +223,7 @@ namespace InteractivityManager {
         }
         }
 
 
         selectOnly(current: Representation.Loci, applyGranularity = true) {
         selectOnly(current: Representation.Loci, applyGranularity = true) {
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 // only deselect for the structure of the given loci
                 // only deselect for the structure of the given loci
                 this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false);
                 this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false);
@@ -232,7 +233,7 @@ namespace InteractivityManager {
         }
         }
 
 
         deselect(current: Representation.Loci, applyGranularity = true) {
         deselect(current: Representation.Loci, applyGranularity = true) {
-            const normalized = this.normalizedLoci(current, applyGranularity);
+            const normalized = this.normalizedLoci(current, applyGranularity, true);
             if (StructureElement.Loci.is(normalized.loci)) {
             if (StructureElement.Loci.is(normalized.loci)) {
                 this.sel.modify('remove', normalized.loci);
                 this.sel.modify('remove', normalized.loci);
             }
             }
@@ -255,8 +256,9 @@ namespace InteractivityManager {
                     // do a full deselect/select for the current structure so visuals that are
                     // do a full deselect/select for the current structure so visuals that are
                     // marked with granularity unequal to 'element' and join/intersect operations
                     // marked with granularity unequal to 'element' and join/intersect operations
                     // are handled properly
                     // are handled properly
-                    super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect, true);
-                    super.mark({ loci: this.sel.getLoci(loci.structure) }, MarkerAction.Select);
+                    const selLoci = this.sel.getLoci(loci.structure);
+                    super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect, !Loci.isEmpty(selLoci));
+                    super.mark({ loci: selLoci }, MarkerAction.Select);
                 } else {
                 } else {
                     super.mark(current, action);
                     super.mark(current, action);
                 }
                 }

+ 48 - 10
src/mol-plugin-state/manager/structure/measurement.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
  */
  */
 
 
 import { StructureElement } from '../../../mol-model/structure';
 import { StructureElement } from '../../../mol-model/structure';
@@ -15,6 +16,7 @@ import { StatefulPluginComponent } from '../../component';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common';
 import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common';
 import { LineParams } from '../../../mol-repr/structure/representation/line';
 import { LineParams } from '../../../mol-repr/structure/representation/line';
+import { Expression } from '../../../mol-script/language/expression';
 
 
 export { StructureMeasurementManager };
 export { StructureMeasurementManager };
 
 
@@ -35,6 +37,7 @@ export interface StructureMeasurementManagerState {
     angles: StructureMeasurementCell[],
     angles: StructureMeasurementCell[],
     dihedrals: StructureMeasurementCell[],
     dihedrals: StructureMeasurementCell[],
     orientations: StructureMeasurementCell[],
     orientations: StructureMeasurementCell[],
+    planes: StructureMeasurementCell[],
     options: StructureMeasurementOptions
     options: StructureMeasurementOptions
 }
 }
 
 
@@ -222,19 +225,25 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
     }
 
 
-    async addOrientation(a: StructureElement.Loci) {
-        const cellA = this.plugin.helpers.substructureParent.get(a.structure);
+    async addOrientation(locis: StructureElement.Loci[]) {
+        const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = [];
+        const dependsOn: string[] = [];
 
 
-        if (!cellA) return;
+        for (let i = 0, il = locis.length; i < il; ++i) {
+            const l = locis[i];
+            const cell = this.plugin.helpers.substructureParent.get(l.structure);
+            if (!cell) continue;
 
 
-        const dependsOn = [cellA.transform.ref];
+            arraySetAdd(dependsOn, cell.transform.ref);
+            selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) });
+        }
+
+        if (selections.length === 0) return;
 
 
         const update = this.getGroup();
         const update = this.getGroup();
         update
         update
             .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
             .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
-                selections: [
-                    { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) },
-                ],
+                selections,
                 isTransitive: true,
                 isTransitive: true,
                 label: 'Orientation'
                 label: 'Orientation'
             }, { dependsOn })
             }, { dependsOn })
@@ -244,6 +253,34 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
         await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
     }
     }
 
 
+    async addPlane(locis: StructureElement.Loci[]) {
+        const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = [];
+        const dependsOn: string[] = [];
+
+        for (let i = 0, il = locis.length; i < il; ++i) {
+            const l = locis[i];
+            const cell = this.plugin.helpers.substructureParent.get(l.structure);
+            if (!cell) continue;
+
+            arraySetAdd(dependsOn, cell.transform.ref);
+            selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) });
+        }
+
+        if (selections.length === 0) return;
+
+        const update = this.getGroup();
+        update
+            .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
+                selections,
+                isTransitive: true,
+                label: 'Plane'
+            }, { dependsOn })
+            .apply(StateTransforms.Representation.StructureSelectionsPlane3D);
+
+        const state = this.plugin.state.data;
+        await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } });
+    }
+
     private _empty: any[] = [];
     private _empty: any[] = [];
     private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) {
     private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) {
         const state = this.plugin.state.data;
         const state = this.plugin.state.data;
@@ -259,13 +296,14 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
             distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D),
             distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D),
             angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D),
             angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D),
             dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D),
             dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D),
-            orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D)
+            orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D),
+            planes: this.getTransforms(StateTransforms.Representation.StructureSelectionsPlane3D),
         });
         });
         if (updated) this.stateUpdated();
         if (updated) this.stateUpdated();
     }
     }
 
 
     constructor(private plugin: PluginContext) {
     constructor(private plugin: PluginContext) {
-        super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], options: DefaultStructureMeasurementOptions });
+        super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], planes: [], options: DefaultStructureMeasurementOptions });
 
 
         plugin.state.data.events.changed.subscribe(e => {
         plugin.state.data.events.changed.subscribe(e => {
             if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return;
             if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return;

+ 4 - 9
src/mol-plugin-state/manager/structure/selection.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
@@ -22,6 +22,7 @@ import { PluginStateObject as PSO } from '../../objects';
 import { UUID } from '../../../mol-util';
 import { UUID } from '../../../mol-util';
 import { StructureRef } from './hierarchy-state';
 import { StructureRef } from './hierarchy-state';
 import { Boundary } from '../../../mol-math/geometry/boundary';
 import { Boundary } from '../../../mol-math/geometry/boundary';
+import { iterableToArray } from '../../../mol-data/util';
 
 
 interface StructureSelectionManagerState {
 interface StructureSelectionManagerState {
     entries: Map<string, SelectionEntry>,
     entries: Map<string, SelectionEntry>,
@@ -405,14 +406,8 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
     }
     }
 
 
     getPrincipalAxes(): PrincipalAxes {
     getPrincipalAxes(): PrincipalAxes {
-        const elementCount = this.elementCount();
-        const positions = new Float32Array(3 * elementCount);
-        let offset = 0;
-        this.entries.forEach(v => {
-            StructureElement.Loci.toPositionsArray(v.selection, positions, offset);
-            offset += StructureElement.Loci.size(v.selection) * 3;
-        });
-        return PrincipalAxes.ofPositions(positions);
+        const values = iterableToArray(this.entries.values());
+        return StructureElement.Loci.getPrincipalAxesMany(values.map(v => v.selection));
     }
     }
 
 
     modify(modifier: StructureSelectionModifier, loci: Loci) {
     modify(modifier: StructureSelectionModifier, loci: Loci) {

+ 7 - 3
src/mol-plugin-state/transforms/helpers.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
  */
  */
@@ -10,6 +10,7 @@ import { LabelData } from '../../mol-repr/shape/loci/label';
 import { OrientationData } from '../../mol-repr/shape/loci/orientation';
 import { OrientationData } from '../../mol-repr/shape/loci/orientation';
 import { AngleData } from '../../mol-repr/shape/loci/angle';
 import { AngleData } from '../../mol-repr/shape/loci/angle';
 import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
 import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
+import { PlaneData } from '../../mol-repr/shape/loci/plane';
 
 
 export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData {
 export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData {
     const lociA = s[0].loci;
     const lociA = s[0].loci;
@@ -38,6 +39,9 @@ export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginState
 }
 }
 
 
 export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData {
 export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData {
-    const loci = s[0].loci;
-    return { locis: [loci] };
+    return { locis: s.map(v => v.loci) };
+}
+
+export function getPlaneDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): PlaneData {
+    return { locis: s.map(v => v.loci) };
 }
 }

+ 2 - 1
src/mol-plugin-state/transforms/model.ts

@@ -645,7 +645,8 @@ const MultiStructureSelectionFromExpression = PluginStateTransform.BuiltIn({
                     totalSize += StructureElement.Loci.size(loci.loci);
                     totalSize += StructureElement.Loci.size(loci.loci);
 
 
                     continue;
                     continue;
-                } if (entry.expression !== sel.expression) {
+                }
+                if (entry.expression !== sel.expression) {
                     recreate = true;
                     recreate = true;
                 } else {
                 } else {
                     // TODO: properly support "transitive" queries. For that Structure.areUnitAndIndicesEqual needs to be fixed;
                     // TODO: properly support "transitive" queries. For that Structure.areUnitAndIndicesEqual needs to be fixed;

+ 36 - 2
src/mol-plugin-state/transforms/representation.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
@@ -28,7 +28,7 @@ import { BaseGeometry } from '../../mol-geo/geometry/base';
 import { Script } from '../../mol-script/script';
 import { Script } from '../../mol-script/script';
 import { UnitcellParams, UnitcellRepresentation, getUnitcellData } from '../../mol-repr/shape/model/unitcell';
 import { UnitcellParams, UnitcellRepresentation, getUnitcellData } from '../../mol-repr/shape/model/unitcell';
 import { DistanceParams, DistanceRepresentation } from '../../mol-repr/shape/loci/distance';
 import { DistanceParams, DistanceRepresentation } from '../../mol-repr/shape/loci/distance';
-import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } from './helpers';
+import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections, getPlaneDataFromStructureSelections } from './helpers';
 import { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label';
 import { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label';
 import { OrientationRepresentation, OrientationParams } from '../../mol-repr/shape/loci/orientation';
 import { OrientationRepresentation, OrientationParams } from '../../mol-repr/shape/loci/orientation';
 import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle';
 import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle';
@@ -40,6 +40,7 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { getBoxMesh } from './shape';
 import { getBoxMesh } from './shape';
 import { Shape } from '../../mol-model/shape';
 import { Shape } from '../../mol-model/shape';
 import { Box3D } from '../../mol-math/geometry';
 import { Box3D } from '../../mol-math/geometry';
+import { PlaneParams, PlaneRepresentation } from '../../mol-repr/shape/loci/plane';
 
 
 export { StructureRepresentation3D };
 export { StructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
 export { ExplodeStructureRepresentation3D };
@@ -986,4 +987,37 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({
             return StateTransformer.UpdateResult.Updated;
             return StateTransformer.UpdateResult.Updated;
         });
         });
     },
     },
+});
+
+export { StructureSelectionsPlane3D };
+type StructureSelectionsPlane3D = typeof StructureSelectionsPlane3D
+const StructureSelectionsPlane3D = PluginStateTransform.BuiltIn({
+    name: 'structure-selections-plane-3d',
+    display: '3D Plane',
+    from: SO.Molecule.Structure.Selections,
+    to: SO.Shape.Representation3D,
+    params: () => ({
+        ...PlaneParams,
+    })
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Structure Plane', async ctx => {
+            const data = getPlaneDataFromStructureSelections(a.data);
+            const repr = PlaneRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => PlaneParams);
+            await repr.createOrUpdate(params, data).runInContext(ctx);
+            return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Plane` });
+        });
+    },
+    update({ a, b, oldParams, newParams }, plugin: PluginContext) {
+        return Task.create('Structure Plane', async ctx => {
+            const props = { ...b.data.repr.props, ...newParams };
+            const data = getPlaneDataFromStructureSelections(a.data);
+            await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
+            b.data.sourceData = data;
+            return StateTransformer.UpdateResult.Updated;
+        });
+    },
 });
 });

+ 2 - 2
src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx

@@ -207,7 +207,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS
 
 
         const updatedPoint = this.unNormalizePoint(Vec2.create(this.updatedX, this.updatedY));
         const updatedPoint = this.unNormalizePoint(Vec2.create(this.updatedX, this.updatedY));
         const points = this.state.points.filter((_, i) => i !== selected[0]);
         const points = this.state.points.filter((_, i) => i !== selected[0]);
-        points.push(updatedPoint);;
+        points.push(updatedPoint);
         points.sort((a, b) => {
         points.sort((a, b) => {
             if (a[0] === b[0]) {
             if (a[0] === b[0]) {
                 if (a[0] === 0) {
                 if (a[0] === 0) {
@@ -372,7 +372,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS
         const data = points;
         const data = points;
         const size = data.length;
         const size = data.length;
 
 
-        for (let i = 0; i < size - 1;i++) {
+        for (let i = 0; i < size - 1; i++) {
             const x1 = data[i][0];
             const x1 = data[i][0];
             const y1 = data[i][1];
             const y1 = data[i][1];
             const x2 = data[i + 1][0];
             const x2 = data[i + 1][0];

+ 5 - 1
src/mol-plugin-ui/sequence/sequence.tsx

@@ -153,7 +153,11 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
     private getBackgroundColor(marker: number) {
     private getBackgroundColor(marker: number) {
         // TODO: make marker color configurable
         // TODO: make marker color configurable
         if (typeof marker === 'undefined') console.error('unexpected marker value');
         if (typeof marker === 'undefined') console.error('unexpected marker value');
-        return marker === 0 ? '' : marker % 2 === 0 ? 'rgb(51, 255, 25)' /* selected */ : 'rgb(255, 102, 153)' /* highlighted */;
+        return marker === 0
+            ? ''
+            : marker % 2 === 0
+                ? 'rgb(51, 255, 25)' // selected
+                : 'rgb(255, 102, 153)'; // highlighted
     }
     }
 
 
     private getResidueClass(seqIdx: number, label: string) {
     private getResidueClass(seqIdx: number, label: string) {

+ 30 - 7
src/mol-plugin-ui/structure/measurements.tsx

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -15,7 +15,8 @@ import { AngleData } from '../../mol-repr/shape/loci/angle';
 import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
 import { DihedralData } from '../../mol-repr/shape/loci/dihedral';
 import { DistanceData } from '../../mol-repr/shape/loci/distance';
 import { DistanceData } from '../../mol-repr/shape/loci/distance';
 import { LabelData } from '../../mol-repr/shape/loci/label';
 import { LabelData } from '../../mol-repr/shape/loci/label';
-import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label';
+import { OrientationData } from '../../mol-repr/shape/loci/orientation';
+import { angleLabel, dihedralLabel, distanceLabel, lociLabel, structureElementLociLabelMany } from '../../mol-theme/label';
 import { FiniteArray } from '../../mol-util/type-helpers';
 import { FiniteArray } from '../../mol-util/type-helpers';
 import { CollapsableControls, PurePluginUIComponent } from '../base';
 import { CollapsableControls, PurePluginUIComponent } from '../base';
 import { ActionMenu } from '../controls/action-menu';
 import { ActionMenu } from '../controls/action-menu';
@@ -67,6 +68,8 @@ export class MeasurementList extends PurePluginUIComponent {
             {this.renderGroup(measurements.distances, 'Distances')}
             {this.renderGroup(measurements.distances, 'Distances')}
             {this.renderGroup(measurements.angles, 'Angles')}
             {this.renderGroup(measurements.angles, 'Angles')}
             {this.renderGroup(measurements.dihedrals, 'Dihedrals')}
             {this.renderGroup(measurements.dihedrals, 'Dihedrals')}
+            {this.renderGroup(measurements.orientations, 'Orientations')}
+            {this.renderGroup(measurements.planes, 'Planes')}
         </div>;
         </div>;
     }
     }
 }
 }
@@ -108,13 +111,31 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
         this.plugin.managers.structure.measurement.addLabel(loci[0].loci);
         this.plugin.managers.structure.measurement.addLabel(loci[0].loci);
     }
     }
 
 
+    addOrientation = () => {
+        const locis: StructureElement.Loci[] = [];
+        this.plugin.managers.structure.selection.entries.forEach(v => {
+            locis.push(v.selection);
+        });
+        this.plugin.managers.structure.measurement.addOrientation(locis);
+    }
+
+    addPlane = () => {
+        const locis: StructureElement.Loci[] = [];
+        this.plugin.managers.structure.selection.entries.forEach(v => {
+            locis.push(v.selection);
+        });
+        this.plugin.managers.structure.measurement.addPlane(locis);
+    }
+
     get actions(): ActionMenu.Items {
     get actions(): ActionMenu.Items {
         const history = this.selection.additionsHistory;
         const history = this.selection.additionsHistory;
         const ret: ActionMenu.Item[] = [
         const ret: ActionMenu.Item[] = [
-            { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 },
-            { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 },
-            { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 },
-            { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 },
+            { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection item required)' : ' (1st selection item)'}`, value: this.addLabel, disabled: history.length === 0 },
+            { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selection items required)' : ' (top 2 selection items)'}`, value: this.measureDistance, disabled: history.length < 2 },
+            { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selection items required)' : ' (top 3 items)'}`, value: this.measureAngle, disabled: history.length < 3 },
+            { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selection items required)' : ' (top 4 selection items)'}`, value: this.measureDihedral, disabled: history.length < 4 },
+            { kind: 'item', label: `Orientation ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addOrientation, disabled: history.length === 0 },
+            { kind: 'item', label: `Plane ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addPlane, disabled: history.length === 0 },
         ];
         ];
         return ret;
         return ret;
     }
     }
@@ -219,7 +240,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
     }
     }
 
 
     get selections() {
     get selections() {
-        return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData> | undefined;
+        return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData & OrientationData> | undefined;
     }
     }
 
 
     delete = () => {
     delete = () => {
@@ -266,6 +287,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         if (selections.pairs) return selections.pairs[0].loci;
         if (selections.pairs) return selections.pairs[0].loci;
         if (selections.triples) return selections.triples[0].loci;
         if (selections.triples) return selections.triples[0].loci;
         if (selections.quads) return selections.quads[0].loci;
         if (selections.quads) return selections.quads[0].loci;
+        if (selections.locis) return selections.locis;
         return [];
         return [];
     }
     }
 
 
@@ -277,6 +299,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
         if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel });
         if (selections.triples) return angleLabel(selections.triples[0], { condensed: true });
         if (selections.triples) return angleLabel(selections.triples[0], { condensed: true });
         if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true });
         if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true });
+        if (selections.locis) return structureElementLociLabelMany(selections.locis, { countsOnly: true });
         return '<empty>';
         return '<empty>';
     }
     }
 
 

+ 26 - 10
src/mol-plugin/behavior/dynamic/representation.ts

@@ -16,7 +16,7 @@ import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observ
 import { Binding } from '../../../mol-util/binding';
 import { Binding } from '../../../mol-util/binding';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
+import { Bond, Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
 import { arrayMax } from '../../../mol-util/array';
 import { arrayMax } from '../../../mol-util/array';
 import { Representation } from '../../../mol-repr/representation';
 import { Representation } from '../../../mol-repr/representation';
 import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
 import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
@@ -34,6 +34,7 @@ const DefaultHighlightLociBindings = {
 const HighlightLociParams = {
 const HighlightLociParams = {
     bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }),
     bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }),
     ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }),
     ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }),
+    preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }),
     mark: PD.Boolean(true)
     mark: PD.Boolean(true)
 };
 };
 type HighlightLociProps = PD.Values<typeof HighlightLociParams>
 type HighlightLociProps = PD.Values<typeof HighlightLociParams>
@@ -46,10 +47,17 @@ export const HighlightLoci = PluginBehavior.create({
             if (!this.ctx.canvas3d || !this.params.mark) return;
             if (!this.ctx.canvas3d || !this.params.mark) return;
             this.ctx.canvas3d.mark(interactionLoci, action, noRender);
             this.ctx.canvas3d.mark(interactionLoci, action, noRender);
         }
         }
+        private getLoci(loci: Loci) {
+            return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2
+                ? Bond.toFirstStructureElementLoci(loci)
+                : loci;
+        }
         register() {
         register() {
             this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
             this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => {
                 if (!this.ctx.canvas3d || this.ctx.isBusy) return;
                 if (!this.ctx.canvas3d || this.ctx.isBusy) return;
-                if (this.params.ignore?.indexOf(current.loci.kind) >= 0) {
+
+                const loci = this.getLoci(current.loci);
+                if (this.params.ignore?.indexOf(loci.kind) >= 0) {
                     this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci });
                     this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci });
                     return;
                     return;
                 }
                 }
@@ -58,13 +66,13 @@ export const HighlightLoci = PluginBehavior.create({
 
 
                 if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
                 if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) {
                     // remove repr to highlight loci everywhere on hover
                     // remove repr to highlight loci everywhere on hover
-                    this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci: current.loci });
+                    this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci });
                     matched = true;
                     matched = true;
                 }
                 }
 
 
                 if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
                 if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) {
                     // remove repr to highlight loci everywhere on hover
                     // remove repr to highlight loci everywhere on hover
-                    this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci: current.loci });
+                    this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci });
                     matched = true;
                     matched = true;
                 }
                 }
 
 
@@ -95,6 +103,7 @@ const DefaultSelectLociBindings = {
 const SelectLociParams = {
 const SelectLociParams = {
     bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }),
     bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }),
     ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }),
     ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }),
+    preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }),
     mark: PD.Boolean(true)
     mark: PD.Boolean(true)
 };
 };
 type SelectLociProps = PD.Values<typeof SelectLociParams>
 type SelectLociProps = PD.Values<typeof SelectLociParams>
@@ -108,6 +117,11 @@ export const SelectLoci = PluginBehavior.create({
             if (!this.ctx.canvas3d || !this.params.mark) return;
             if (!this.ctx.canvas3d || !this.params.mark) return;
             this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action, noRender);
             this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action, noRender);
         }
         }
+        private getLoci(loci: Loci) {
+            return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2
+                ? Bond.toFirstStructureElementLoci(loci)
+                : loci;
+        }
         private applySelectMark(ref: string, clear?: boolean) {
         private applySelectMark(ref: string, clear?: boolean) {
             const cell = this.ctx.state.data.cells.get(ref);
             const cell = this.ctx.state.data.cells.get(ref);
             if (cell && SO.isRepresentation3D(cell.obj)) {
             if (cell && SO.isRepresentation3D(cell.obj)) {
@@ -123,10 +137,10 @@ export const SelectLoci = PluginBehavior.create({
             }
             }
         }
         }
         register() {
         register() {
-            const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
-            const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
+            const lociIsEmpty = (loci: Loci) => Loci.isEmpty(loci);
+            const lociIsNotEmpty = (loci: Loci) => !Loci.isEmpty(loci);
 
 
-            const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
+            const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Loci) => boolean) | undefined][] = [
                 ['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty],
                 ['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty],
                 ['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty],
                 ['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty],
                 ['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty],
                 ['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty],
@@ -145,12 +159,14 @@ export const SelectLoci = PluginBehavior.create({
 
 
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
                 if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode) return;
                 if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode) return;
-                if (this.params.ignore?.indexOf(current.loci.kind) >= 0) return;
+
+                const loci = this.getLoci(current.loci);
+                if (this.params.ignore?.indexOf(loci.kind) >= 0) return;
 
 
                 // only trigger the 1st action that matches
                 // only trigger the 1st action that matches
                 for (const [binding, action, condition] of actions) {
                 for (const [binding, action, condition] of actions) {
-                    if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
-                        action(current);
+                    if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(loci))) {
+                        action({ repr: current.repr, loci });
                         break;
                         break;
                     }
                     }
                 }
                 }

+ 1 - 1
src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts

@@ -25,7 +25,7 @@ const StructureFocusRepresentationParams = (plugin: PluginContext) => {
         expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
         expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
         targetParams: PD.Group(reprParams, {
         targetParams: PD.Group(reprParams, {
             label: 'Target',
             label: 'Target',
-            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.26, alpha: 0.51 } })
+            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.26, alpha: 0.51, adjustCylinderLength: true } })
         }),
         }),
         surroundingsParams: PD.Group(reprParams, {
         surroundingsParams: PD.Group(reprParams, {
             label: 'Surroundings',
             label: 'Surroundings',

+ 14 - 0
src/mol-plugin/config.ts

@@ -19,6 +19,16 @@ export class PluginConfigItem<T = any> {
 
 
 function item<T>(key: string, defaultValue?: T) { return new PluginConfigItem(key, defaultValue); }
 function item<T>(key: string, defaultValue?: T) { return new PluginConfigItem(key, defaultValue); }
 
 
+
+// adapted from https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
+function is_iOS() {
+    if (typeof navigator === 'undefined' || typeof window === 'undefined') return false;
+    const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
+    const isAppleDevice = navigator.userAgent.includes('Macintosh');
+    const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
+    return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen));
+}
+
 export const PluginConfig = {
 export const PluginConfig = {
     item,
     item,
     General: {
     General: {
@@ -27,7 +37,11 @@ export const PluginConfig = {
         DisablePreserveDrawingBuffer: item('plugin-config.disable-preserve-drawing-buffer', false),
         DisablePreserveDrawingBuffer: item('plugin-config.disable-preserve-drawing-buffer', false),
         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),
         EnableWboit: item('plugin-config.enable-wboit', true),
         EnableWboit: item('plugin-config.enable-wboit', true),
+        // 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
+        PreferWebGl1: item('plugin-config.prefer-webgl1', is_iOS()),
     },
     },
     State: {
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

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

@@ -196,8 +196,11 @@ export class PluginContext {
                 const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false);
                 const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false);
                 const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
                 const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
                 const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
                 const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
+                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;
-                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit });
+                const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false;
+                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit, preferWebGl1 });
+                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit });
             }
             }
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             this.canvas3dInit.next(true);
             this.canvas3dInit.next(true);

+ 9 - 4
src/mol-repr/representation.ts

@@ -65,12 +65,16 @@ export namespace RepresentationProvider {
 
 
 export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State>
 export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State>
 
 
-const EmptyRepresentationProvider = {
+export const EmptyRepresentationProvider: RepresentationProvider = {
+    name: '',
     label: '',
     label: '',
     description: '',
     description: '',
     factory: () => Representation.Empty,
     factory: () => Representation.Empty,
     getParams: () => ({}),
     getParams: () => ({}),
-    defaultValues: {}
+    defaultValues: {},
+    defaultColorTheme: ColorTheme.EmptyProvider,
+    defaultSizeTheme: SizeTheme.EmptyProvider,
+    isApplicable: () => true
 };
 };
 
 
 function getTypes(list: { name: string, provider: RepresentationProvider<any, any, any> }[]) {
 function getTypes(list: { name: string, provider: RepresentationProvider<any, any, any> }[]) {
@@ -114,7 +118,7 @@ export class RepresentationRegistry<D, S extends Representation.State> {
     }
     }
 
 
     get<P extends PD.Params>(name: string): RepresentationProvider<D, P, S> {
     get<P extends PD.Params>(name: string): RepresentationProvider<D, P, S> {
-        return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P, S>;
+        return this._map.get(name) || EmptyRepresentationProvider;
     }
     }
 
 
     get list() {
     get list() {
@@ -226,7 +230,7 @@ namespace Representation {
         let version = 0;
         let version = 0;
         const updated = new Subject<number>();
         const updated = new Subject<number>();
         const currentState = stateBuilder.create();
         const currentState = stateBuilder.create();
-        const currentTheme = Theme.createEmpty();
+        let currentTheme = Theme.createEmpty();
 
 
         let currentParams: P;
         let currentParams: P;
         let currentProps: PD.Values<P>;
         let currentProps: PD.Values<P>;
@@ -314,6 +318,7 @@ namespace Representation {
                 }
                 }
             },
             },
             setTheme: (theme: Theme) => {
             setTheme: (theme: Theme) => {
+                currentTheme = theme;
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     reprList[i].setTheme(theme);
                     reprList[i].setTheme(theme);
                 }
                 }

+ 45 - 57
src/mol-repr/shape/loci/orientation.ts

@@ -1,10 +1,9 @@
 /**
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 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>
  */
  */
 
 
-import { Loci } from '../../../mol-model/loci';
 import { RuntimeContext } from '../../../mol-task';
 import { RuntimeContext } from '../../../mol-task';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ColorNames } from '../../../mol-util/color/names';
 import { ColorNames } from '../../../mol-util/color/names';
@@ -13,21 +12,23 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro
 import { Shape } from '../../../mol-model/shape';
 import { Shape } from '../../../mol-model/shape';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
 import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
-import { lociLabel } from '../../../mol-theme/label';
+import { structureElementLociLabelMany } from '../../../mol-theme/label';
 import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes';
 import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes';
 import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box';
 import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box';
 import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
 import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
 import { Axes3D } from '../../../mol-math/geometry';
 import { Axes3D } from '../../../mol-math/geometry';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { MarkerActions } from '../../../mol-util/marker-action';
 import { MarkerActions } from '../../../mol-util/marker-action';
+import { StructureElement } from '../../../mol-model/structure';
 
 
 export interface OrientationData {
 export interface OrientationData {
-    locis: Loci[]
+    locis: StructureElement.Loci[]
 }
 }
 
 
 const SharedParams = {
 const SharedParams = {
     color: PD.Color(ColorNames.orange),
     color: PD.Color(ColorNames.orange),
-    scale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 })
+    scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+    radiusScale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 })
 };
 };
 
 
 const AxesParams = {
 const AxesParams = {
@@ -57,97 +58,84 @@ const OrientationVisuals = {
 export const OrientationParams = {
 export const OrientationParams = {
     ...AxesParams,
     ...AxesParams,
     ...BoxParams,
     ...BoxParams,
+    ...EllipsoidParams,
     visuals: PD.MultiSelect(['box'], PD.objectToOptions(OrientationVisuals)),
     visuals: PD.MultiSelect(['box'], PD.objectToOptions(OrientationVisuals)),
-    color: PD.Color(ColorNames.orange),
-    scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
 };
 };
 export type OrientationParams = typeof OrientationParams
 export type OrientationParams = typeof OrientationParams
 export type OrientationProps = PD.Values<OrientationParams>
 export type OrientationProps = PD.Values<OrientationParams>
 
 
 //
 //
 
 
-function orientationLabel(loci: Loci) {
-    const label = lociLabel(loci, { countsOnly: true });
+function getAxesName(locis: StructureElement.Loci[]) {
+    const label = structureElementLociLabelMany(locis, { countsOnly: true });
     return `Principal Axes of ${label}`;
     return `Principal Axes of ${label}`;
 }
 }
 
 
-function getOrientationName(data: OrientationData) {
-    return data.locis.length === 1 ? orientationLabel(data.locis[0]) : `${data.locis.length} Orientations`;
-}
-
-//
-
 function buildAxesMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
 function buildAxesMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
     const state = MeshBuilder.createState(256, 128, mesh);
     const state = MeshBuilder.createState(256, 128, mesh);
-    for (let i = 0, il = data.locis.length; i < il; ++i) {
-        const principalAxes = Loci.getPrincipalAxes(data.locis[i]);
-        if (principalAxes) {
-            state.currentGroup = i;
-            addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20);
-        }
-    }
+    const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis);
+    Axes3D.scale(principalAxes.momentsAxes, principalAxes.momentsAxes, props.scaleFactor);
+
+    state.currentGroup = 0;
+    addAxes(state, principalAxes.momentsAxes, props.radiusScale, 2, 20);
     return MeshBuilder.getMesh(state);
     return MeshBuilder.getMesh(state);
 }
 }
 
 
 function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
 function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
     const mesh = buildAxesMesh(data, props, shape && shape.geometry);
     const mesh = buildAxesMesh(data, props, shape && shape.geometry);
-    const name = getOrientationName(data);
-    const getLabel = function (groupId: number) {
-        return orientationLabel(data.locis[groupId]);
-    };
-    return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel);
+    const name = getAxesName(data.locis);
+    return Shape.create(name, data, mesh, () => props.color, () => 1, () => name);
 }
 }
 
 
 //
 //
 
 
+function getBoxName(locis: StructureElement.Loci[]) {
+    const label = structureElementLociLabelMany(locis, { countsOnly: true });
+    return `Oriented Box of ${label}`;
+}
+
 function buildBoxMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
 function buildBoxMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
     const state = MeshBuilder.createState(256, 128, mesh);
     const state = MeshBuilder.createState(256, 128, mesh);
-    for (let i = 0, il = data.locis.length; i < il; ++i) {
-        const principalAxes = Loci.getPrincipalAxes(data.locis[i]);
-        if (principalAxes) {
-            state.currentGroup = i;
-            addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20);
-        }
-    }
+    const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis);
+    Axes3D.scale(principalAxes.boxAxes, principalAxes.boxAxes, props.scaleFactor);
+
+    state.currentGroup = 0;
+    addOrientedBox(state, principalAxes.boxAxes, props.radiusScale, 2, 20);
     return MeshBuilder.getMesh(state);
     return MeshBuilder.getMesh(state);
 }
 }
 
 
 function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
 function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
     const mesh = buildBoxMesh(data, props, shape && shape.geometry);
     const mesh = buildBoxMesh(data, props, shape && shape.geometry);
-    const name = getOrientationName(data);
-    const getLabel = function (groupId: number) {
-        return orientationLabel(data.locis[groupId]);
-    };
-    return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel);
+    const name = getBoxName(data.locis);
+    return Shape.create(name, data, mesh, () => props.color, () => 1, () => name);
 }
 }
 
 
 //
 //
 
 
+function getEllipsoidName(locis: StructureElement.Loci[]) {
+    const label = structureElementLociLabelMany(locis, { countsOnly: true });
+    return `Oriented Ellipsoid of ${label}`;
+}
+
 function buildEllipsoidMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
 function buildEllipsoidMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh {
     const state = MeshBuilder.createState(256, 128, mesh);
     const state = MeshBuilder.createState(256, 128, mesh);
-    for (let i = 0, il = data.locis.length; i < il; ++i) {
-        const principalAxes = Loci.getPrincipalAxes(data.locis[i]);
-        if (principalAxes) {
-            const axes = principalAxes.boxAxes;
-            const { origin, dirA, dirB } = axes;
-            const size = Axes3D.size(Vec3(), axes);
-            Vec3.scale(size, size, 0.5);
-            const radiusScale = Vec3.create(size[2], size[1], size[0]);
-
-            state.currentGroup = i;
-            addEllipsoid(state, origin, dirA, dirB, radiusScale, 2);
-        }
-    }
+    const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis);
+
+    const axes = principalAxes.boxAxes;
+    const { origin, dirA, dirB } = axes;
+    const size = Axes3D.size(Vec3(), axes);
+    Vec3.scale(size, size, 0.5 * props.scaleFactor);
+    const radiusScale = Vec3.create(size[2], size[1], size[0]);
+
+    state.currentGroup = 0;
+    addEllipsoid(state, origin, dirA, dirB, radiusScale, 2);
     return MeshBuilder.getMesh(state);
     return MeshBuilder.getMesh(state);
 }
 }
 
 
 function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
 function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) {
     const mesh = buildEllipsoidMesh(data, props, shape && shape.geometry);
     const mesh = buildEllipsoidMesh(data, props, shape && shape.geometry);
-    const name = getOrientationName(data);
-    const getLabel = function (groupId: number) {
-        return orientationLabel(data.locis[groupId]);
-    };
-    return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel);
+    const name = getEllipsoidName(data.locis);
+    return Shape.create(name, data, mesh, () => props.color, () => 1, () => name);
 }
 }
 
 
 //
 //

+ 84 - 0
src/mol-repr/shape/loci/plane.ts

@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { RuntimeContext } from '../../../mol-task';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { ColorNames } from '../../../mol-util/color/names';
+import { ShapeRepresentation } from '../representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation';
+import { Shape } from '../../../mol-model/shape';
+import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
+import { structureElementLociLabelMany } from '../../../mol-theme/label';
+import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
+import { MarkerActions } from '../../../mol-util/marker-action';
+import { Plane } from '../../../mol-geo/primitive/plane';
+import { StructureElement } from '../../../mol-model/structure';
+import { Axes3D } from '../../../mol-math/geometry';
+
+export interface PlaneData {
+    locis: StructureElement.Loci[]
+}
+
+const _PlaneParams = {
+    ...Mesh.Params,
+    color: PD.Color(ColorNames.orange),
+    scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
+};
+type _PlaneParams = typeof _PlaneParams
+
+const PlaneVisuals = {
+    'plane': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, _PlaneParams>) => ShapeRepresentation(getPlaneShape, Mesh.Utils),
+};
+
+export const PlaneParams = {
+    ..._PlaneParams,
+    visuals: PD.MultiSelect(['plane'], PD.objectToOptions(PlaneVisuals)),
+};
+export type PlaneParams = typeof PlaneParams
+export type PlaneProps = PD.Values<PlaneParams>
+
+//
+
+function getPlaneName(locis: StructureElement.Loci[]) {
+    const label = structureElementLociLabelMany(locis, { countsOnly: true });
+    return `Best Fit Plane of ${label}`;
+}
+
+const tmpMat = Mat4();
+const tmpV = Vec3();
+function buildPlaneMesh(data: PlaneData, props: PlaneProps, mesh?: Mesh): Mesh {
+    const state = MeshBuilder.createState(256, 128, mesh);
+    const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis);
+    const axes = principalAxes.boxAxes;
+    const plane = Plane();
+
+    Vec3.add(tmpV, axes.origin, axes.dirC);
+    Mat4.targetTo(tmpMat, tmpV, axes.origin, axes.dirB);
+    Mat4.scale(tmpMat, tmpMat, Axes3D.size(tmpV, axes));
+    Mat4.scaleUniformly(tmpMat, tmpMat, props.scaleFactor);
+    Mat4.setTranslation(tmpMat, axes.origin);
+
+    state.currentGroup = 0;
+    MeshBuilder.addPrimitive(state, tmpMat, plane);
+    MeshBuilder.addPrimitiveFlipped(state, tmpMat, plane);
+    return MeshBuilder.getMesh(state);
+}
+
+function getPlaneShape(ctx: RuntimeContext, data: PlaneData, props: PlaneProps, shape?: Shape<Mesh>) {
+    const mesh = buildPlaneMesh(data, props, shape && shape.geometry);
+    const name = getPlaneName(data.locis);
+    return Shape.create(name, data, mesh, () => props.color, () => 1, () => name);
+}
+
+//
+
+export type PlaneRepresentation = Representation<PlaneData, PlaneParams>
+export function PlaneRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, PlaneParams>): PlaneRepresentation {
+    const repr = Representation.createMulti('Plane', ctx, getParams, Representation.StateBuilder, PlaneVisuals as unknown as Representation.Def<PlaneData, PlaneParams>);
+    repr.setState({ markerActions: MarkerActions.Highlighting });
+    return repr;
+}

+ 14 - 5
src/mol-repr/structure/representation/line.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 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>
  */
  */
@@ -14,23 +14,32 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro
 import { ThemeRegistryContext } from '../../../mol-theme/theme';
 import { ThemeRegistryContext } from '../../../mol-theme/theme';
 import { Structure } from '../../../mol-model/structure';
 import { Structure } from '../../../mol-model/structure';
 import { getUnitKindsParam } from '../params';
 import { getUnitKindsParam } from '../params';
+import { ElementPointParams, ElementPointVisual } from '../visual/element-point';
+import { ElementCrossParams, ElementCrossVisual } from '../visual/element-cross';
 
 
 const LineVisuals = {
 const LineVisuals = {
     'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondLineParams>) => UnitsRepresentation('Intra-unit bond line', ctx, getParams, IntraUnitBondLineVisual),
     'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondLineParams>) => UnitsRepresentation('Intra-unit bond line', ctx, getParams, IntraUnitBondLineVisual),
     'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondLineParams>) => ComplexRepresentation('Inter-unit bond line', ctx, getParams, InterUnitBondLineVisual),
     'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondLineParams>) => ComplexRepresentation('Inter-unit bond line', ctx, getParams, InterUnitBondLineVisual),
+    'element-point': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementPointParams>) => UnitsRepresentation('Points', ctx, getParams, ElementPointVisual),
+    'element-cross': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementCrossParams>) => UnitsRepresentation('Crosses', ctx, getParams, ElementCrossVisual),
 };
 };
 
 
 export const LineParams = {
 export const LineParams = {
     ...IntraUnitBondLineParams,
     ...IntraUnitBondLineParams,
     ...InterUnitBondLineParams,
     ...InterUnitBondLineParams,
+    ...ElementPointParams,
+    ...ElementCrossParams,
+    multipleBonds: PD.Select('offset', PD.arrayToOptions(['off', 'symmetric', 'offset'] as const)),
     includeParent: PD.Boolean(false),
     includeParent: PD.Boolean(false),
-    sizeFactor: PD.Numeric(1.5, { min: 0.01, max: 10, step: 0.01 }),
+    sizeFactor: PD.Numeric(3, { min: 0.01, max: 10, step: 0.01 }),
     unitKinds: getUnitKindsParam(['atomic']),
     unitKinds: getUnitKindsParam(['atomic']),
-    visuals: PD.MultiSelect(['intra-bond', 'inter-bond'], PD.objectToOptions(LineVisuals))
+    visuals: PD.MultiSelect(['intra-bond', 'inter-bond', 'element-point', 'element-cross'], PD.objectToOptions(LineVisuals))
 };
 };
 export type LineParams = typeof LineParams
 export type LineParams = typeof LineParams
 export function getLineParams(ctx: ThemeRegistryContext, structure: Structure) {
 export function getLineParams(ctx: ThemeRegistryContext, structure: Structure) {
-    return PD.clone(LineParams);
+    const params = PD.clone(LineParams);
+    params.pointStyle.defaultValue = 'circle';
+    return params;
 }
 }
 
 
 export type LineRepresentation = StructureRepresentation<LineParams>
 export type LineRepresentation = StructureRepresentation<LineParams>
@@ -41,7 +50,7 @@ export function LineRepresentation(ctx: RepresentationContext, getParams: Repres
 export const LineRepresentationProvider = StructureRepresentationProvider({
 export const LineRepresentationProvider = StructureRepresentationProvider({
     name: 'line',
     name: 'line',
     label: 'Line',
     label: 'Line',
-    description: 'Displays bonds as lines.',
+    description: 'Displays bonds as lines and atoms as points or croses.',
     factory: LineRepresentation,
     factory: LineRepresentation,
     getParams: getLineParams,
     getParams: getLineParams,
     defaultValues: PD.getDefaultValues(LineParams),
     defaultValues: PD.getDefaultValues(LineParams),

+ 2 - 2
src/mol-repr/structure/representation/point.ts

@@ -32,11 +32,11 @@ export function PointRepresentation(ctx: RepresentationContext, getParams: Repre
 export const PointRepresentationProvider = StructureRepresentationProvider({
 export const PointRepresentationProvider = StructureRepresentationProvider({
     name: 'point',
     name: 'point',
     label: 'Point',
     label: 'Point',
-    description: 'Displays elements (atoms, coarse spheres) as spheres.',
+    description: 'Displays elements (atoms, coarse spheres) as points.',
     factory: PointRepresentation,
     factory: PointRepresentation,
     getParams: getPointParams,
     getParams: getPointParams,
     defaultValues: PD.getDefaultValues(PointParams),
     defaultValues: PD.getDefaultValues(PointParams),
     defaultColorTheme: { name: 'element-symbol' },
     defaultColorTheme: { name: 'element-symbol' },
-    defaultSizeTheme: { name: 'physical' },
+    defaultSizeTheme: { name: 'uniform' },
     isApplicable: (structure: Structure) => structure.elementCount > 0
     isApplicable: (structure: Structure) => structure.elementCount > 0
 });
 });

+ 20 - 7
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -40,7 +40,10 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
 
 
     const bonds = structure.interUnitBonds;
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props;
+    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, multipleBonds } = props;
+
+    const mbOff = multipleBonds === 'off';
+    const mbSymmetric = multipleBonds === 'symmetric';
 
 
     const delta = Vec3();
     const delta = Vec3();
 
 
@@ -136,14 +139,16 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
                 return LinkStyle.Dashed;
             } else if (o === 3) {
             } else if (o === 3) {
-                return LinkStyle.Triple;
+                return mbOff ? LinkStyle.Solid :
+                    mbSymmetric ? LinkStyle.Triple :
+                        LinkStyle.OffsetTriple;
             } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
             } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
                 return LinkStyle.Aromatic;
                 return LinkStyle.Aromatic;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             }
             }
 
 
-            return LinkStyle.Solid;
+            return (o !== 2 || mbOff) ? LinkStyle.Solid :
+                mbSymmetric ? LinkStyle.Double :
+                    LinkStyle.OffsetDouble;
         },
         },
         radius: (edgeIndex: number) => {
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;
             return radius(edgeIndex) * sizeAspectRatio;
@@ -210,13 +215,17 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.aromaticScale !== currentProps.aromaticScale ||
+                newProps.aromaticSpacing !== currentProps.aromaticSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 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.stubCap !== currentProps.stubCap ||
                 newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
-                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
+                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
@@ -248,13 +257,17 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.aromaticScale !== currentProps.aromaticScale ||
+                newProps.aromaticSpacing !== currentProps.aromaticSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 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.stubCap !== currentProps.stubCap ||
                 newProps.stubCap !== currentProps.stubCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
-                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
+                newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {

+ 14 - 6
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -32,10 +32,14 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
 function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
 function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) {
     const bonds = structure.interUnitBonds;
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
     const { edgeCount, edges } = bonds;
-    const { sizeFactor, aromaticBonds } = props;
 
 
     if (!edgeCount) return Lines.createEmpty(lines);
     if (!edgeCount) return Lines.createEmpty(lines);
 
 
+    const { sizeFactor, aromaticBonds, multipleBonds } = props;
+
+    const mbOff = multipleBonds === 'off';
+    const mbSymmetric = multipleBonds === 'symmetric';
+
     const ref = Vec3();
     const ref = Vec3();
     const loc = StructureElement.Location.create();
     const loc = StructureElement.Location.create();
 
 
@@ -74,14 +78,16 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
                 return LinkStyle.Dashed;
             } else if (o === 3) {
             } else if (o === 3) {
-                return LinkStyle.Triple;
+                return mbOff ? LinkStyle.Solid :
+                    mbSymmetric ? LinkStyle.Triple :
+                        LinkStyle.OffsetTriple;
             } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
             } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) {
                 return LinkStyle.Aromatic;
                 return LinkStyle.Aromatic;
-            } else if (o === 2) {
-                return LinkStyle.Double;
             }
             }
 
 
-            return LinkStyle.Solid;
+            return (o !== 2 || mbOff) ? LinkStyle.Solid :
+                mbSymmetric ? LinkStyle.Double :
+                    LinkStyle.OffsetDouble;
         },
         },
         radius: (edgeIndex: number) => {
         radius: (edgeIndex: number) => {
             const b = edges[edgeIndex];
             const b = edges[edgeIndex];
@@ -125,10 +131,12 @@ export function InterUnitBondLineVisual(materialId: number): ComplexVisual<Inter
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
-                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
             if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {

+ 20 - 5
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -33,7 +33,10 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
     const bonds = unit.bonds;
     const bonds = unit.bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { edgeCount, a, b, edgeProps, offset } = bonds;
     const { order: _order, flags: _flags } = edgeProps;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, includeTypes, excludeTypes } = props;
+    const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, includeTypes, excludeTypes, multipleBonds } = props;
+
+    const mbOff = multipleBonds === 'off';
+    const mbSymmetric = multipleBonds === 'symmetric';
 
 
     const include = BondType.fromNames(includeTypes);
     const include = BondType.fromNames(includeTypes);
     const exclude = BondType.fromNames(excludeTypes);
     const exclude = BondType.fromNames(excludeTypes);
@@ -130,7 +133,9 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
                 return LinkStyle.Dashed;
             } else if (o === 3) {
             } else if (o === 3) {
-                return LinkStyle.Triple;
+                return mbOff ? LinkStyle.Solid :
+                    mbSymmetric ? LinkStyle.Triple :
+                        LinkStyle.OffsetTriple;
             } else if (aromaticBonds) {
             } else if (aromaticBonds) {
                 const aI = a[edgeIndex], bI = b[edgeIndex];
                 const aI = a[edgeIndex], bI = b[edgeIndex];
                 const aR = elementAromaticRingIndices.get(aI);
                 const aR = elementAromaticRingIndices.get(aI);
@@ -146,7 +151,9 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru
                 }
                 }
             }
             }
 
 
-            return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
+            return (o !== 2 || mbOff) ? LinkStyle.Solid :
+                mbSymmetric ? LinkStyle.Double :
+                    LinkStyle.OffsetDouble;
         },
         },
         radius: (edgeIndex: number) => {
         radius: (edgeIndex: number) => {
             return radius(edgeIndex) * sizeAspectRatio;
             return radius(edgeIndex) * sizeAspectRatio;
@@ -221,6 +228,9 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.aromaticScale !== currentProps.aromaticScale ||
+                newProps.aromaticSpacing !== currentProps.aromaticSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
@@ -228,7 +238,8 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
-                newProps.aromaticBonds !== currentProps.aromaticBonds
+                newProps.aromaticBonds !== currentProps.aromaticBonds ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             const newUnit = newStructureGroup.group.units[0];
             const newUnit = newStructureGroup.group.units[0];
@@ -264,6 +275,9 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.aromaticScale !== currentProps.aromaticScale ||
+                newProps.aromaticSpacing !== currentProps.aromaticSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
@@ -271,7 +285,8 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength ||
-                newProps.aromaticBonds !== currentProps.aromaticBonds
+                newProps.aromaticBonds !== currentProps.aromaticBonds ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             const newUnit = newStructureGroup.group.units[0];
             const newUnit = newStructureGroup.group.units[0];

+ 13 - 4
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -39,7 +39,10 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
     if (!edgeCount) return Lines.createEmpty(lines);
     if (!edgeCount) return Lines.createEmpty(lines);
 
 
     const { order: _order, flags: _flags } = edgeProps;
     const { order: _order, flags: _flags } = edgeProps;
-    const { sizeFactor, aromaticBonds, includeTypes, excludeTypes } = props;
+    const { sizeFactor, aromaticBonds, includeTypes, excludeTypes, multipleBonds } = props;
+
+    const mbOff = multipleBonds === 'off';
+    const mbSymmetric = multipleBonds === 'symmetric';
 
 
     const include = BondType.fromNames(includeTypes);
     const include = BondType.fromNames(includeTypes);
     const exclude = BondType.fromNames(excludeTypes);
     const exclude = BondType.fromNames(excludeTypes);
@@ -91,7 +94,9 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 // show metallic coordinations and hydrogen bonds with dashed cylinders
                 return LinkStyle.Dashed;
                 return LinkStyle.Dashed;
             } else if (o === 3) {
             } else if (o === 3) {
-                return LinkStyle.Triple;
+                return mbOff ? LinkStyle.Solid :
+                    mbSymmetric ? LinkStyle.Triple :
+                        LinkStyle.OffsetTriple;
             } else if (aromaticBonds) {
             } else if (aromaticBonds) {
                 const aI = a[edgeIndex], bI = b[edgeIndex];
                 const aI = a[edgeIndex], bI = b[edgeIndex];
                 const aR = elementAromaticRingIndices.get(aI);
                 const aR = elementAromaticRingIndices.get(aI);
@@ -107,7 +112,9 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str
                 }
                 }
             }
             }
 
 
-            return o === 2 ? LinkStyle.Double : LinkStyle.Solid;
+            return (o !== 2 || mbOff) ? LinkStyle.Solid :
+                mbSymmetric ? LinkStyle.Double :
+                    LinkStyle.OffsetDouble;
         },
         },
         radius: (edgeIndex: number) => {
         radius: (edgeIndex: number) => {
             location.element = elements[a[edgeIndex]];
             location.element = elements[a[edgeIndex]];
@@ -146,11 +153,13 @@ export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUn
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.aromaticDashCount !== currentProps.aromaticDashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
-                newProps.aromaticBonds !== currentProps.aromaticBonds
+                newProps.aromaticBonds !== currentProps.aromaticBonds ||
+                newProps.multipleBonds !== currentProps.multipleBonds
             );
             );
 
 
             const newUnit = newStructureGroup.group.units[0];
             const newUnit = newStructureGroup.group.units[0];

+ 94 - 0
src/mol-repr/structure/visual/element-cross.ts

@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual } from '../units-visual';
+import { VisualContext } from '../../visual';
+import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
+import { Theme } from '../../../mol-theme/theme';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+import { ElementIterator, getElementLoci, eachElement, makeElementIgnoreTest } from './util/element';
+import { VisualUpdateState } from '../../util';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { Lines } from '../../../mol-geo/geometry/lines/lines';
+import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
+import { bondCount } from '../../../mol-model-props/computed/chemistry/util';
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3scaleAndAdd = Vec3.scaleAndAdd;
+const v3unitX = Vec3.unitX;
+const v3unitY = Vec3.unitY;
+const v3unitZ = Vec3.unitZ;
+
+export const ElementCrossParams = {
+    ...UnitsLinesParams,
+    lineSizeAttenuation: PD.Boolean(false),
+    ignoreHydrogens: PD.Boolean(false),
+    traceOnly: PD.Boolean(false),
+    crosses: PD.Select('lone', PD.arrayToOptions(['lone', 'all'] as const)),
+    crossSize: PD.Numeric(0.35, { min: 0, max: 2, step: 0.01 }),
+};
+export type ElementCrossParams = typeof ElementCrossParams
+
+export function createElementCross(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ElementCrossParams>, lines: Lines) {
+    const { child } = structure;
+    if (child && !child.unitMap.get(unit.id)) return Lines.createEmpty(lines);
+
+    const elements = unit.elements;
+    const n = elements.length;
+    const builder = LinesBuilder.create(n, n / 10, lines);
+
+    const p = Vec3();
+    const s = Vec3();
+    const e = Vec3();
+
+    const pos = unit.conformation.invariantPosition;
+    const ignore = makeElementIgnoreTest(structure, unit, props);
+
+    const r = props.crossSize / 2;
+    const lone = props.crosses === 'lone';
+
+    for (let i = 0 as StructureElement.UnitIndex; i < n; ++i) {
+        if (ignore && ignore(elements[i])) continue;
+        if (lone && Unit.isAtomic(unit) && bondCount(structure, unit, i) !== 0) continue;
+
+        pos(elements[i], p);
+        v3scaleAndAdd(s, p, v3unitX, r);
+        v3scaleAndAdd(e, p, v3unitX, -r);
+        builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i);
+        v3scaleAndAdd(s, p, v3unitY, r);
+        v3scaleAndAdd(e, p, v3unitY, -r);
+        builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i);
+        v3scaleAndAdd(s, p, v3unitZ, r);
+        v3scaleAndAdd(e, p, v3unitZ, -r);
+        builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i);
+    }
+
+    const l = builder.getLines();
+
+    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
+    l.setBoundingSphere(sphere);
+
+    return l;
+}
+
+export function ElementCrossVisual(materialId: number): UnitsVisual<ElementCrossParams> {
+    return UnitsLinesVisual<ElementCrossParams>({
+        defaultProps: PD.getDefaultValues(ElementCrossParams),
+        createGeometry: createElementCross,
+        createLocationIterator: ElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        eachLocation: eachElement,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementCrossParams>, currentProps: PD.Values<ElementCrossParams>) => {
+            state.createGeometry = (
+                newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
+                newProps.traceOnly !== currentProps.traceOnly ||
+                newProps.crosses !== currentProps.crosses ||
+                newProps.crossSize !== currentProps.crossSize
+            );
+        }
+    }, materialId);
+}

+ 1 - 1
src/mol-repr/structure/visual/element-point.ts

@@ -18,7 +18,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
 
 
 export const ElementPointParams = {
 export const ElementPointParams = {
     ...UnitsPointsParams,
     ...UnitsPointsParams,
-    pointSizeAttenuation: PD.Boolean(true),
+    pointSizeAttenuation: PD.Boolean(false),
     ignoreHydrogens: PD.Boolean(false),
     ignoreHydrogens: PD.Boolean(false),
     traceOnly: PD.Boolean(false),
     traceOnly: PD.Boolean(false),
 };
 };

+ 2 - 1
src/mol-repr/structure/visual/util/bond.ts

@@ -20,6 +20,7 @@ export const BondParams = {
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
     excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)),
     ignoreHydrogens: PD.Boolean(false),
     ignoreHydrogens: PD.Boolean(false),
     aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }),
     aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }),
+    multipleBonds: PD.Select('symmetric', PD.arrayToOptions(['off', 'symmetric', 'offset'] as const)),
 };
 };
 export const DefaultBondProps = PD.getDefaultValues(BondParams);
 export const DefaultBondProps = PD.getDefaultValues(BondParams);
 export type BondProps = typeof DefaultBondProps
 export type BondProps = typeof DefaultBondProps
@@ -27,7 +28,7 @@ export type BondProps = typeof DefaultBondProps
 export const BondCylinderParams = {
 export const BondCylinderParams = {
     ...LinkCylinderParams,
     ...LinkCylinderParams,
     ...BondParams,
     ...BondParams,
-    adjustCylinderLength: PD.Boolean(true, { description: 'Shorten cylinders to reduce overlap with spheres.' })
+    adjustCylinderLength: PD.Boolean(false, { description: 'Shorten cylinders to reduce overlap with spheres. Useful for for transparent bonds. Not working well with aromatic bonds.' })
 };
 };
 export const DefaultBondCylinderProps = PD.getDefaultValues(BondCylinderParams);
 export const DefaultBondCylinderProps = PD.getDefaultValues(BondCylinderParams);
 export type BondCylinderProps = typeof DefaultBondCylinderProps
 export type BondCylinderProps = typeof DefaultBondCylinderProps

+ 96 - 32
src/mol-repr/structure/visual/util/link.ts

@@ -21,6 +21,9 @@ export const LinkCylinderParams = {
     linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }),
     linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
     linkCap: PD.Boolean(false),
     linkCap: PD.Boolean(false),
+    aromaticScale: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }),
+    aromaticSpacing: PD.Numeric(1.5, { min: 0, max: 3, step: 0.01 }),
+    aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }),
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
     dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }),
     dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }),
     dashCap: PD.Boolean(true),
     dashCap: PD.Boolean(true),
@@ -33,6 +36,7 @@ export type LinkCylinderProps = typeof DefaultLinkCylinderProps
 export const LinkLineParams = {
 export const LinkLineParams = {
     linkScale: PD.Numeric(0.5, { min: 0, max: 1, step: 0.1 }),
     linkScale: PD.Numeric(0.5, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(0.1, { min: 0, max: 2, step: 0.01 }),
     linkSpacing: PD.Numeric(0.1, { min: 0, max: 2, step: 0.01 }),
+    aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }),
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
     dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
 };
 };
 export const DefaultLinkLineProps = PD.getDefaultValues(LinkLineParams);
 export const DefaultLinkLineProps = PD.getDefaultValues(LinkLineParams);
@@ -83,10 +87,12 @@ export const enum LinkStyle {
     Solid = 0,
     Solid = 0,
     Dashed = 1,
     Dashed = 1,
     Double = 2,
     Double = 2,
-    Triple = 3,
-    Disk = 4,
-    Aromatic = 5,
-    MirroredAromatic = 6,
+    OffsetDouble = 3,
+    Triple = 4,
+    OffsetTriple = 5,
+    Disk = 6,
+    Aromatic = 7,
+    MirroredAromatic = 8,
 }
 }
 
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
@@ -105,7 +111,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
 
 
     if (!linkCount) return Mesh.createEmpty(mesh);
     if (!linkCount) return Mesh.createEmpty(mesh);
 
 
-    const { linkScale, linkSpacing, radialSegments, linkCap, dashCount, dashScale, dashCap, stubCap } = props;
+    const { linkScale, linkSpacing, radialSegments, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props;
 
 
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2;
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2;
     const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh);
     const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh);
@@ -128,14 +134,15 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
 
 
         position(va, vb, edgeIndex);
         position(va, vb, edgeIndex);
         v3sub(tmpV12, vb, va);
         v3sub(tmpV12, vb, va);
+        const dirFlag = v3dot(tmpV12, up) > 0;
 
 
         const linkRadius = radius(edgeIndex);
         const linkRadius = radius(edgeIndex);
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
         const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
         const linkStub = stubCap && (stub ? stub(edgeIndex) : false);
-        const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [linkStub, linkCap] : [linkCap, linkStub];
+        const [topCap, bottomCap] = dirFlag ? [linkStub, linkCap] : [linkCap, linkStub];
         builderState.currentGroup = edgeIndex;
         builderState.currentGroup = edgeIndex;
 
 
-        const aromaticOffsetFactor = 4.5;
+        const aromaticSegmentCount = aromaticDashCount + 1;
 
 
         if (linkStyle === LinkStyle.Solid) {
         if (linkStyle === LinkStyle.Solid) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
@@ -148,9 +155,9 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
             cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
             cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
 
 
             addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
             addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
-            const order = linkStyle === LinkStyle.Double ? 2 :
-                linkStyle === LinkStyle.Triple ? 3 : 1.5;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 :
+                (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5;
             const multiRadius = linkRadius * (linkScale / (0.5 * order));
             const multiRadius = linkRadius * (linkScale / (0.5 * order));
             const absOffset = (linkRadius - multiRadius) * linkSpacing;
             const absOffset = (linkRadius - multiRadius) * linkSpacing;
 
 
@@ -163,18 +170,49 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
                 cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
                 cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
                 addCylinder(builderState, va, vb, 0.5, cylinderProps);
                 addCylinder(builderState, va, vb, 0.5, cylinderProps);
 
 
-                cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * linkScale;
+                const aromaticOffset = linkRadius + aromaticScale * linkRadius + aromaticScale * linkRadius * aromaticSpacing;
+
+                v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), linkRadius * 0.5);
+                v3add(va, va, tmpV12);
+                v3sub(vb, vb, tmpV12);
+
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * aromaticScale;
                 cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
                 cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
-                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3setMagnitude(vShift, vShift, aromaticOffset);
                 v3sub(va, va, vShift);
                 v3sub(va, va, vShift);
                 v3sub(vb, vb, vShift);
                 v3sub(vb, vb, vShift);
-                addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
+                addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps);
 
 
                 if (linkStyle === LinkStyle.MirroredAromatic) {
                 if (linkStyle === LinkStyle.MirroredAromatic) {
-                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3setMagnitude(vShift, vShift, aromaticOffset * 2);
+                    v3add(va, va, vShift);
+                    v3add(vb, vb, vShift);
+                    addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps);
+                }
+            } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
+                const multipleOffset = linkRadius + multiRadius + linkScale * linkRadius * linkSpacing;
+                v3setMagnitude(vShift, vShift, multipleOffset);
+
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
+                addCylinder(builderState, va, vb, 0.5, cylinderProps);
+
+                v3scale(tmpV12, tmpV12, linkSpacing * linkScale * 0.2);
+                v3add(va, va, tmpV12);
+                v3sub(vb, vb, tmpV12);
+
+                cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
+                cylinderProps.topCap = dirFlag ? linkStub : dashCap;
+                cylinderProps.bottomCap = dirFlag ? dashCap : linkStub;
+                v3setMagnitude(vShift, vShift, multipleOffset);
+                v3sub(va, va, vShift);
+                v3sub(vb, vb, vShift);
+                addCylinder(builderState, va, vb, 0.5, cylinderProps);
+
+                if (order === 3) {
+                    v3setMagnitude(vShift, vShift, multipleOffset * 2);
                     v3add(va, va, vShift);
                     v3add(va, va, vShift);
                     v3add(vb, vb, vShift);
                     v3add(vb, vb, vShift);
-                    addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps);
+                    addCylinder(builderState, va, vb, 0.5, cylinderProps);
                 }
                 }
             } else {
             } else {
                 v3setMagnitude(vShift, vShift, absOffset);
                 v3setMagnitude(vShift, vShift, absOffset);
@@ -208,7 +246,7 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
 
 
     if (!linkCount) return Cylinders.createEmpty(cylinders);
     if (!linkCount) return Cylinders.createEmpty(cylinders);
 
 
-    const { linkScale, linkSpacing, linkCap, dashCount, dashScale, dashCap, stubCap } = props;
+    const { linkScale, linkSpacing, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props;
 
 
     const cylindersCountEstimate = linkCount * 2;
     const cylindersCountEstimate = linkCount * 2;
     const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders);
     const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders);
@@ -222,9 +260,8 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
 
 
-    const aromaticSegmentCount = 3;
+    const aromaticSegmentCount = aromaticDashCount + 1;
     const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
     const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
-    const aromaticOffsetFactor = 4.5;
 
 
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
         if (ignore && ignore(edgeIndex)) continue;
@@ -242,9 +279,9 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3sub(vb, vb, tmpV12);
             v3sub(vb, vb, tmpV12);
             builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
             builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
-            const order = linkStyle === LinkStyle.Double ? 2 :
-                linkStyle === LinkStyle.Triple ? 3 : 1.5;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 :
+                (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5;
             const multiScale = linkScale / (0.5 * order);
             const multiScale = linkScale / (0.5 * order);
             const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing;
             const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing;
 
 
@@ -254,24 +291,40 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin
             if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
             if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
                 builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
                 builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
 
 
+                const aromaticOffset = linkRadius + aromaticScale * linkRadius + aromaticScale * linkRadius * aromaticSpacing;
+
                 v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
                 v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale);
                 v3sub(vb, vb, tmpV12);
                 v3sub(vb, vb, tmpV12);
 
 
-                v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor);
+                v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), linkRadius * 0.5);
+                v3add(va, va, tmpV12);
+
+                v3setMagnitude(vShift, vShift, aromaticOffset);
                 v3sub(va, va, vShift);
                 v3sub(va, va, vShift);
                 v3sub(vb, vb, vShift);
                 v3sub(vb, vb, vShift);
-                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
+                builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex);
 
 
                 if (linkStyle === LinkStyle.MirroredAromatic) {
                 if (linkStyle === LinkStyle.MirroredAromatic) {
-                    v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2);
+                    v3setMagnitude(vShift, vShift, aromaticOffset * 2);
                     v3add(va, va, vShift);
                     v3add(va, va, vShift);
                     v3add(vb, vb, vShift);
                     v3add(vb, vb, vShift);
-                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex);
+                    builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex);
                 }
                 }
+            } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
+                const multipleOffset = linkRadius + multiScale * linkRadius + linkScale * linkRadius * linkSpacing;
+                v3setMagnitude(vShift, vShift, multipleOffset);
+
+                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex);
+
+                v3setMagnitude(tmpV12, v3sub(tmpV12, va, vb), linkRadius / 1.5);
+                v3sub(va, va, tmpV12);
+
+                if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
+                builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, dashCap, linkStub, edgeIndex);
             } else {
             } else {
                 v3setMagnitude(vShift, vShift, absOffset);
                 v3setMagnitude(vShift, vShift, absOffset);
 
 
-                if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, false, edgeIndex);
+                if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, linkStub, edgeIndex);
                 builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
                 builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex);
                 builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
                 builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex);
             }
             }
@@ -296,7 +349,7 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
 
 
     if (!linkCount) return Lines.createEmpty(lines);
     if (!linkCount) return Lines.createEmpty(lines);
 
 
-    const { linkScale, linkSpacing, dashCount } = props;
+    const { linkScale, linkSpacing, aromaticDashCount, dashCount } = props;
 
 
     const linesCountEstimate = linkCount * 2;
     const linesCountEstimate = linkCount * 2;
     const builder = LinesBuilder.create(linesCountEstimate, linesCountEstimate / 4, lines);
     const builder = LinesBuilder.create(linesCountEstimate, linesCountEstimate / 4, lines);
@@ -310,9 +363,10 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
     const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
 
 
-    const aromaticSegmentCount = 3;
+    const aromaticSegmentCount = aromaticDashCount + 1;
     const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
     const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount);
     const aromaticOffsetFactor = 4.5;
     const aromaticOffsetFactor = 4.5;
+    const multipleOffsetFactor = 3;
 
 
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
         if (ignore && ignore(edgeIndex)) continue;
@@ -328,9 +382,9 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
             v3sub(vb, vb, tmpV12);
             v3sub(vb, vb, tmpV12);
             builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
             builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
-        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
-            const order = linkStyle === LinkStyle.Double ? 2 :
-                linkStyle === LinkStyle.Triple ? 3 : 1.5;
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) {
+            const order = linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble ? 2 :
+                linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple ? 3 : 1.5;
             const multiRadius = 1 * (linkScale / (0.5 * order));
             const multiRadius = 1 * (linkScale / (0.5 * order));
             const absOffset = (1 - multiRadius) * linkSpacing;
             const absOffset = (1 - multiRadius) * linkSpacing;
 
 
@@ -354,8 +408,18 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
                     v3add(vb, vb, vShift);
                     v3add(vb, vb, vShift);
                     builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
                     builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex);
                 }
                 }
+            } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) {
+                v3setMagnitude(vShift, vShift, absOffset * multipleOffsetFactor);
+
+                builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
+
+                v3scale(tmpV12, v3sub(tmpV12, va, vb), linkSpacing * linkScale);
+                v3sub(va, va, tmpV12);
+
+                if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);
+                builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], edgeIndex);
             } else {
             } else {
-                v3setMagnitude(vShift, vShift, absOffset);
+                v3setMagnitude(vShift, vShift, absOffset * 1.5);
 
 
                 if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
                 if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex);
                 builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);
                 builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex);

+ 8 - 0
src/mol-theme/label.ts

@@ -92,6 +92,14 @@ export function structureElementStatsLabel(stats: StructureElement.Stats, option
     return o.htmlStyling ? label : stripTags(label);
     return o.htmlStyling ? label : stripTags(label);
 }
 }
 
 
+export function structureElementLociLabelMany(locis: StructureElement.Loci[], options: Partial<LabelOptions> = {}): string {
+    const stats = StructureElement.Stats.create();
+    for (const l of locis) {
+        StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(l));
+    }
+    return structureElementStatsLabel(stats, options);
+}
+
 function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string {
 function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string {
     const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats;
     const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats;
 
 

+ 10 - 2
src/mol-util/marker-action.ts

@@ -68,11 +68,19 @@ export function applyMarkerAction(array: Uint8Array, set: OrderedSet, action: Ma
     if (Interval.is(set)) {
     if (Interval.is(set)) {
         const start = Interval.start(set);
         const start = Interval.start(set);
         const end = Interval.end(set);
         const end = Interval.end(set);
-        const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2);
-
         const viewStart = (start + 3) >> 2;
         const viewStart = (start + 3) >> 2;
         const viewEnd = viewStart + ((end - 4 * viewStart) >> 2);
         const viewEnd = viewStart + ((end - 4 * viewStart) >> 2);
 
 
+        if (viewEnd <= viewStart) {
+            // avoid edge cases with overlapping front/end intervals
+            for (let i = start; i < end; ++i) {
+                applyMarkerActionAtPosition(array, i, action);
+            }
+            return true;
+        }
+
+        const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2);
+
         const frontStart = start;
         const frontStart = start;
         const frontEnd = Math.min(4 * viewStart, end);
         const frontEnd = Math.min(4 * viewStart, end);
         const backStart = Math.max(start, 4 * viewEnd);
         const backStart = Math.max(start, 4 * viewEnd);

+ 1 - 1
src/mol-util/zip/bin.ts

@@ -92,7 +92,7 @@ export function sizeUTF8(str: string) {
     for (let ci = 0; ci < strl; ci++) {
     for (let ci = 0; ci < strl; ci++) {
         const code = str.charCodeAt(ci);
         const code = str.charCodeAt(ci);
         if ((code & (0xffffffff - (1 << 7) + 1)) === 0) {
         if ((code & (0xffffffff - (1 << 7) + 1)) === 0) {
-            i++ ;
+            i++;
         } else if ((code & (0xffffffff - (1 << 11) + 1)) === 0) {
         } else if ((code & (0xffffffff - (1 << 11) + 1)) === 0) {
             i += 2;
             i += 2;
         } else if ((code & (0xffffffff - (1 << 16) + 1)) === 0) {
         } else if ((code & (0xffffffff - (1 << 16) + 1)) === 0) {

+ 3 - 0
src/servers/model/CHANGELOG.md

@@ -1,3 +1,6 @@
+# 0.9.8
+* fix support for chem_comp_bond and struct_conn categories
+
 # 0.9.7
 # 0.9.7
 * add Surrounding Ligands query
 * add Surrounding Ligands query
 
 

+ 5 - 5
src/servers/model/config.ts

@@ -229,10 +229,10 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) {
             'JSON config file path',
             'JSON config file path',
             'If a property is not specified, cmd line param/OS variable/default value are used.'
             'If a property is not specified, cmd line param/OS variable/default value are used.'
         ].join('\n'),
         ].join('\n'),
-        required: false
+        required: false,
     });
     });
-    parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, nargs: 0 });
-    parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, nargs: 0 });
+    parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, action: 'store_true' });
+    parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, action: 'store_true' });
 }
 }
 
 
 function setConfig(config: ModelServerConfig) {
 function setConfig(config: ModelServerConfig) {
@@ -271,7 +271,7 @@ function parseConfigArguments() {
 export function configureServer() {
 export function configureServer() {
     const config = parseConfigArguments();
     const config = parseConfigArguments();
 
 
-    if (config.cfgTemplate !== null) {
+    if (!!config.cfgTemplate) {
         console.log(JSON.stringify(ModelServerConfigTemplate, null, 2));
         console.log(JSON.stringify(ModelServerConfigTemplate, null, 2));
         process.exit(0);
         process.exit(0);
     }
     }
@@ -284,7 +284,7 @@ export function configureServer() {
             setConfig(cfg);
             setConfig(cfg);
         }
         }
 
 
-        if (config.printCfg !== null) {
+        if (!!config.printCfg) {
             console.log(JSON.stringify(ModelServerConfig, null, 2));
             console.log(JSON.stringify(ModelServerConfig, null, 2));
             process.exit(0);
             process.exit(0);
         }
         }

+ 2 - 2
src/servers/model/version.ts

@@ -1,7 +1,7 @@
 /**
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 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>
  */
  */
 
 
-export const VERSION = '0.9.7';
+export const VERSION = '0.9.8';

+ 6 - 6
src/servers/volume/config.ts

@@ -89,10 +89,10 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) {
             'JSON config file path',
             'JSON config file path',
             'If a property is not specified, cmd line param/OS variable/default value are used.'
             'If a property is not specified, cmd line param/OS variable/default value are used.'
         ].join('\n'),
         ].join('\n'),
-        required: false
+        required: false,
     });
     });
-    parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, nargs: 0 });
-    parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, nargs: 0 });
+    parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, action: 'store_true' });
+    parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, action: 'store_true' });
 }
 }
 
 
 export interface ServerJsonConfig {
 export interface ServerJsonConfig {
@@ -165,7 +165,7 @@ export function configureServer() {
     addLimitsArgs(parser);
     addLimitsArgs(parser);
     const config = parser.parse_args() as FullServerConfig & ServerJsonConfig;
     const config = parser.parse_args() as FullServerConfig & ServerJsonConfig;
 
 
-    if (config.cfgTemplate !== null) {
+    if (!!config.cfgTemplate) {
         console.log(JSON.stringify(ServerConfigTemplate, null, 2));
         console.log(JSON.stringify(ServerConfigTemplate, null, 2));
         process.exit(0);
         process.exit(0);
     }
     }
@@ -178,7 +178,7 @@ export function configureServer() {
             setConfig(cfg);
             setConfig(cfg);
         }
         }
 
 
-        if (config.printCfg !== null) {
+        if (config.printCfg) {
             console.log(JSON.stringify({ ...ServerConfig, ...LimitsConfig }, null, 2));
             console.log(JSON.stringify({ ...ServerConfig, ...LimitsConfig }, null, 2));
             process.exit(0);
             process.exit(0);
         }
         }
@@ -197,7 +197,7 @@ export function configureLocal() {
         description: VOLUME_SERVER_HEADER
         description: VOLUME_SERVER_HEADER
     });
     });
     parser.add_argument('--jobs', { help: `Path to a JSON file with job specification.`, required: false });
     parser.add_argument('--jobs', { help: `Path to a JSON file with job specification.`, required: false });
-    parser.add_argument('--jobsTemplate', { help: 'Print example template for jobs.json and exit.', required: false, nargs: 0 });;
+    parser.add_argument('--jobsTemplate', { help: 'Print example template for jobs.json and exit.', required: false, nargs: 0 });
     addJsonConfigArgs(parser);
     addJsonConfigArgs(parser);
     addLimitsArgs(parser);
     addLimitsArgs(parser);
 
 

Vissa filer visades inte eftersom för många filer har ändrats