浏览代码

Merge branch 'master' of https://github.com/molstar/molstar into clipping

Alexander Rose 3 年之前
父节点
当前提交
76ed2e9e11
共有 100 个文件被更改,包括 5403 次插入3836 次删除
  1. 26 2
      .eslintrc.json
  2. 0 18
      .github/workflows/lint.yml
  3. 20 0
      .github/workflows/node.yml
  4. 5 1
      .npmignore
  5. 150 15
      CHANGELOG.md
  6. 13 2
      README.md
  7. 16 23
      data/cif-field-names/cif-core-field-names.csv
  8. 462 280
      package-lock.json
  9. 55 50
      package.json
  10. 41 0
      scripts/clean.js
  11. 10 0
      scripts/deploy.js
  12. 17 17
      src/apps/docking-viewer/viewport.tsx
  13. 1 1
      src/apps/viewer/embedded.html
  14. 11 0
      src/apps/viewer/index.html
  15. 78 8
      src/apps/viewer/index.ts
  16. 6 6
      src/cli/chem-comp-dict/create-ions.ts
  17. 9 9
      src/cli/chem-comp-dict/create-table.ts
  18. 6 6
      src/cli/cif2bcif/index.ts
  19. 23 23
      src/cli/cifschema/index.ts
  20. 22 22
      src/cli/cifschema/util/cif-dic.ts
  21. 4 4
      src/cli/cifschema/util/generate.ts
  22. 2 2
      src/cli/cifschema/util/helper.ts
  23. 2 2
      src/cli/cifschema/util/schema.ts
  24. 5 5
      src/cli/lipid-params/index.ts
  25. 15 15
      src/cli/structure-info/model.ts
  26. 5 5
      src/cli/structure-info/volume.ts
  27. 1 1
      src/examples/domain-annotation-server/mapping.ts
  28. 1 1
      src/examples/domain-annotation-server/server.ts
  29. 3 2
      src/examples/lighting/index.ts
  30. 1 1
      src/examples/proteopedia-wrapper/index.ts
  31. 38 19
      src/extensions/anvil/algorithm.ts
  32. 2 2
      src/extensions/anvil/behavior.ts
  33. 3 3
      src/extensions/cellpack/color/generate.ts
  34. 1 1
      src/extensions/cellpack/color/provided.ts
  35. 7 7
      src/extensions/cellpack/curve.ts
  36. 38 4
      src/extensions/cellpack/data.ts
  37. 1 1
      src/extensions/cellpack/index.ts
  38. 180 81
      src/extensions/cellpack/model.ts
  39. 5 6
      src/extensions/cellpack/preset.ts
  40. 2 2
      src/extensions/cellpack/property.ts
  41. 70 0
      src/extensions/cellpack/representation.ts
  42. 193 17
      src/extensions/cellpack/state.ts
  43. 35 3
      src/extensions/cellpack/util.ts
  44. 2 2
      src/extensions/dnatco/confal-pyramids/behavior.ts
  45. 3 3
      src/extensions/dnatco/confal-pyramids/color.ts
  46. 1 1
      src/extensions/dnatco/confal-pyramids/util.ts
  47. 1 1
      src/extensions/g3d/model.ts
  48. 3 5
      src/extensions/geo-export/controls.ts
  49. 60 66
      src/extensions/geo-export/glb-exporter.ts
  50. 147 12
      src/extensions/geo-export/mesh-exporter.ts
  51. 32 47
      src/extensions/geo-export/obj-exporter.ts
  52. 6 5
      src/extensions/geo-export/ui.tsx
  53. 46 61
      src/extensions/geo-export/usdz-exporter.ts
  54. 1 1
      src/extensions/mp4-export/controls.ts
  55. 2 1
      src/extensions/mp4-export/ui.tsx
  56. 1 1
      src/extensions/pdbe/preferred-assembly.ts
  57. 1 1
      src/extensions/pdbe/structure-quality-report/behavior.ts
  58. 1 1
      src/extensions/pdbe/structure-quality-report/color.ts
  59. 1 1
      src/extensions/pdbe/structure-quality-report/prop.ts
  60. 2 2
      src/extensions/rcsb/assembly-symmetry/behavior.ts
  61. 1 1
      src/extensions/rcsb/assembly-symmetry/prop.ts
  62. 1 1
      src/extensions/rcsb/assembly-symmetry/representation.ts
  63. 1 1
      src/extensions/rcsb/assembly-symmetry/ui.tsx
  64. 10 9
      src/extensions/rcsb/graphql/codegen.yml
  65. 2864 2772
      src/extensions/rcsb/graphql/types.ts
  66. 1 1
      src/extensions/rcsb/validation-report/behavior.ts
  67. 6 6
      src/extensions/rcsb/validation-report/prop.ts
  68. 4 4
      src/extensions/rcsb/validation-report/representation.ts
  69. 26 4
      src/mol-canvas3d/camera.ts
  70. 17 19
      src/mol-canvas3d/camera/util.ts
  71. 22 14
      src/mol-canvas3d/canvas3d.ts
  72. 21 8
      src/mol-canvas3d/controls/trackball.ts
  73. 1 1
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  74. 50 8
      src/mol-canvas3d/helper/interaction-events.ts
  75. 30 29
      src/mol-canvas3d/passes/draw.ts
  76. 3 1
      src/mol-canvas3d/passes/image.ts
  77. 194 0
      src/mol-canvas3d/passes/marking.ts
  78. 29 25
      src/mol-canvas3d/passes/multi-sample.ts
  79. 17 3
      src/mol-canvas3d/passes/pick.ts
  80. 12 7
      src/mol-canvas3d/passes/postprocessing.ts
  81. 2 2
      src/mol-canvas3d/passes/smaa.ts
  82. 1 1
      src/mol-canvas3d/util.ts
  83. 1 1
      src/mol-data/db/_spec/table.spec.ts
  84. 4 4
      src/mol-data/generic/_spec/linked-list.spec.ts
  85. 1 1
      src/mol-data/int/impl/interval.ts
  86. 1 1
      src/mol-data/int/impl/ordered-set.ts
  87. 7 4
      src/mol-data/int/impl/sorted-array.ts
  88. 1 0
      src/mol-data/int/sorted-array.ts
  89. 7 1
      src/mol-data/int/tuple.ts
  90. 5 5
      src/mol-data/util/_spec/chunked-array.spec.ts
  91. 1 1
      src/mol-data/util/chunked-array.ts
  92. 10 1
      src/mol-data/util/hash-functions.ts
  93. 53 8
      src/mol-geo/geometry/base.ts
  94. 10 1
      src/mol-geo/geometry/cylinders/cylinders.ts
  95. 14 4
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  96. 8 2
      src/mol-geo/geometry/image/image.ts
  97. 5 2
      src/mol-geo/geometry/lines/lines.ts
  98. 64 1
      src/mol-geo/geometry/marker-data.ts
  99. 2 3
      src/mol-geo/geometry/mesh/builder/cylinder.ts
  100. 1 1
      src/mol-geo/geometry/mesh/builder/ribbon.ts

+ 26 - 2
.eslintrc.json

@@ -38,7 +38,24 @@
                 "selector": "ExportDefaultDeclaration",
                 "selector": "ExportDefaultDeclaration",
                 "message": "Default exports are not allowed"
                 "message": "Default exports are not allowed"
             }
             }
-        ]
+        ],
+        "no-throw-literal": "error",
+        "key-spacing": "error",
+        "object-curly-spacing": ["error", "always"],
+        "array-bracket-spacing": "error",
+        "space-in-parens": "error",
+        "computed-property-spacing": "error",
+        "prefer-const": ["error", {
+            "destructuring": "all",
+            "ignoreReadBeforeAssign": false
+        }],
+        "space-before-function-paren": "off",
+        "func-call-spacing": "off",
+        "no-multi-spaces": "error",
+        "block-spacing": "error",
+        "keyword-spacing": "off",
+        "space-before-blocks": "error",
+        "semi-spacing": "error"
     },
     },
     "overrides": [
     "overrides": [
         {
         {
@@ -89,7 +106,14 @@
                     "error",
                     "error",
                     "1tbs", { "allowSingleLine": true }
                     "1tbs", { "allowSingleLine": true }
                 ],
                 ],
-                "@typescript-eslint/comma-spacing": "error"
+                "@typescript-eslint/comma-spacing": "error",
+                "@typescript-eslint/space-before-function-paren": ["error", {
+                    "anonymous": "always",
+                    "named": "never",
+                    "asyncArrow": "always"
+                }],
+                "@typescript-eslint/func-call-spacing": ["error"],
+                "@typescript-eslint/keyword-spacing": ["error"]
             }
             }
         }
         }
     ]
     ]

+ 0 - 18
.github/workflows/lint.yml

@@ -1,18 +0,0 @@
-on:
-  push:
-  pull_request:
-
-jobs:
-  eslint:
-    name: eslint
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v1
-    - name: install node v12
-      uses: actions/setup-node@v1
-      with:
-        node-version: 12
-    - name: yarn install
-      run: yarn install
-    - name: eslint
-      uses: icrawl/action-eslint@v1

+ 20 - 0
.github/workflows/node.yml

@@ -0,0 +1,20 @@
+on:
+  push:
+  pull_request:
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-node@v2
+      with:
+        node-version: 14
+    - run: npm ci
+    - run: sudo apt-get install xvfb
+    - name: Lint
+      run: npm run lint
+    - name: Test
+      run: xvfb-run --auto-servernum npm run jest
+    - name: Build
+      run: npm run build

+ 5 - 1
.npmignore

@@ -1 +1,5 @@
-tsconfig.commonjs.tsbuildinfo
+tests
+perf-tests
+_spec
+*.tsbuildinfo
+*.js.map

+ 150 - 15
CHANGELOG.md

@@ -6,11 +6,153 @@ Note that since we don't clearly distinguish between a public and private interf
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
-- Add surronding atoms (5 Angstrom) structure selection query
+- Add ``bumpiness`` (per-object and per-group), ``bumpFrequency`` & ``bumpAmplitude`` (per-object) render parameters (#299)
+- Change ``label`` representation defaults: Use text border instead of rectangle background
+- Add outline color option to renderer
+- Fix false positives in Model.isFromPdbArchive
+- Add drag and drop support for loading any file, including multiple at once
+    - If there are session files (.molx or .molj) among the dropped files, only the first session will be loaded
+
+## [v3.0.0-dev.3] - 2021-12-4
+
+- Fix OBJ and USDZ export
+
+## [v3.0.0-dev.2] - 2021-12-1
+
+- Do not include tests and source maps in NPM package
+
+## [v3.0.0-dev.0] - 2021-11-28
+
+- Add multiple lights support (with color, intensity, and direction parameters)
+- [Breaking] Add per-object material rendering properties
+  - ``SimpleSettingsParams.lighting.renderStyle`` and ``RendererParams.style`` were removed
+- Add substance theme with per-group material rendering properties
+- ``StructureComponentManager.Options`` state saving support
+- ``ParamDefinition.Group.presets`` support
+
+## [v2.4.1] - 2021-11-28
+
+- Fix: allow atoms in aromatic rings to do hydrogen bonds
+
+## [v2.4.0] - 2021-11-25
+
+- Fix secondary-structure property handling
+    - StructureElement.Property was incorrectly resolving type & key
+    - StructureSelectionQuery helpers 'helix' & 'beta' were not ensuring property availability
+- Re-enable VAO with better workaround (bind null elements buffer before deleting)
+- Add ``Representation.geometryVersion`` (increments whenever the geometry of any of its visuals changes)
+- Add support for grid-based smoothing of Overpaint and Transparency visual state for surfaces
+
+## [v2.3.9] - 2021-11-20
+
+- Workaround: switch off VAO support for now
+
+## [v2.3.8] - 2021-11-20
+
+- Fix double canvas context creation (in plugin context)
+- Fix unused vertex attribute handling (track which are used, disable the rest)
+- Workaround for VAO issue in Chrome 96 (can cause WebGL to crash on geometry updates)
+
+## [v2.3.7] - 2021-11-15
+
+- Added ``ViewerOptions.collapseRightPanel``
+- Added ``Viewer.loadTrajectory`` to support loading "composed" trajectories (e.g. from gro + xtc)
+- Fix: handle parent in Structure.remapModel
+- Add ``rounded`` and ``square`` helix profile options to Cartoon representation (in addition to the default ``elliptical``)
+
+## [v2.3.6] - 2021-11-8
+
+- 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
+- CellPack extension update
+    - add binary model support
+    - add compartment (including membrane) geometry support
+    - add latest mycoplasma model example
+- Prefer WebGL1 in Safari 15.1.
+
+## [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
+
+- Take include/exclude flags into account when displaying aromatic bonds
+- Improve marking performance
+    - Avoid unnecessary draw calls/ui updates when marking
+    - Check if loci is superset of visual
+    - Check if loci overlaps with unit visual
+    - Ensure ``Interval`` is used for ranges instead of ``SortedArray``
+    - Add uniform marker type
+    - Special case for reversing previous mark
+- Add optional marking pass
+    - Outlines visible and hidden parts of highlighted/selected groups
+    - Add highlightStrength/selectStrength renderer params
+
+## [v2.2.3] - 2021-08-25
+
+- Add ``invertCantorPairing`` helper function
+- Add ``Mesh`` processing helper ``.smoothEdges``
+- Smooth border of molecular-surface with ``includeParent`` enabled
+- Hide ``includeParent`` option from gaussian-surface visuals (not particularly useful)
+- Improved ``StructureElement.Loci.size`` performance (for marking large cellpack models)
+- Fix new ``TransformData`` issues (camera/bounding helper not showing up)
+- Improve marking performance (avoid superfluous calls to ``StructureElement.Loci.isWholeStructure``)
+
+## [v2.2.2] - 2021-08-11
+
+- Fix ``TransformData`` issues [#133](https://github.com/molstar/molstar/issues/133)
+- Fix ``mol-script`` query compiler const expression recognition.
+
+## [v2.2.1] - 2021-08-02
+
+- Add surrounding atoms (5 Angstrom) structure selection query
 - [Breaking] Add maxDistance prop to ``IndexPairBonds``
 - [Breaking] Add maxDistance prop to ``IndexPairBonds``
 - Fix coordinateSystem not handled in ``Structure.asParent``
 - Fix coordinateSystem not handled in ``Structure.asParent``
-- Add dynamicBonds to ``Structure`` props (force re-calc on model change)
+- Add ``dynamicBonds`` to ``Structure`` props (force re-calc on model change)
     - Expose as optional param in root structure transform helper
     - Expose as optional param in root structure transform helper
+- Add overpaint support to geometry exporters
+- ``InputObserver`` improvements
+  - normalize wheel speed across browsers/platforms
+  - support Safari gestures (used by ``TrackballControls``)
+  - ``PinchInput.fractionDelta`` and use it in ``TrackballControls``
 
 
 ## [v2.2.0] - 2021-07-31
 ## [v2.2.0] - 2021-07-31
 
 
@@ -77,29 +219,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.

+ 13 - 2
README.md

@@ -68,6 +68,17 @@ If working on just the viewer, ``npm run watch-viewer`` will provide shorter com
 
 
 Debug/production mode in browsers can be turned on/off during runtime by calling ``setMolStarDebugMode(true/false, true/false)`` from the dev console.
 Debug/production mode in browsers can be turned on/off during runtime by calling ``setMolStarDebugMode(true/false, true/false)`` from the dev console.
 
 
+### Cleaning and forcing a full rebuild
+    npm run clean
+
+Wipes the `build` and `lib` directories and `.tsbuildinfo` files.
+
+    npm run rebuild
+
+Runs the cleanup script prior to building the project, forcing a full rebuild of the project.
+
+Use these commands to resolve occassional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
+
 ### Build for production:
 ### Build for production:
     NODE_ENV=production npm run build
     NODE_ENV=production npm run build
 
 
@@ -122,9 +133,9 @@ and navigate to `build/viewer`
 
 
 **Convert any CIF to BinaryCIF**
 **Convert any CIF to BinaryCIF**
 
 
-    node lib/servers/model/preprocess -i file.cif -ob file.bcif
+    node lib/commonjs/servers/model/preprocess -i file.cif -ob file.bcif
 
 
-To see all available commands, use ``node lib/servers/model/preprocess -h``.
+To see all available commands, use ``node lib/commonjs/servers/model/preprocess -h``.
 
 
 Or
 Or
 
 

+ 16 - 23
data/cif-field-names/cif-core-field-names.csv

@@ -2,11 +2,11 @@ audit.block_doi
 
 
 database_code.depnum_ccdc_archive
 database_code.depnum_ccdc_archive
 database_code.depnum_ccdc_fiz
 database_code.depnum_ccdc_fiz
-database_code.ICSD
-database_code.MDF
-database_code.NBS
-database_code.CSD
-database_code.COD
+database_code.icsd
+database_code.mdf
+database_code.nbs
+database_code.csd
+database_code.cod
 
 
 chemical.name_systematic
 chemical.name_systematic
 chemical.name_common
 chemical.name_common
@@ -24,8 +24,8 @@ atom_type_scat.dispersion_imag
 atom_type_scat.source
 atom_type_scat.source
 
 
 space_group.crystal_system
 space_group.crystal_system
-space_group.name_H-M_full
-space_group.IT_number
+space_group.name_h-m_full
+space_group.it_number
 space_group_symop.operation_xyz
 space_group_symop.operation_xyz
 
 
 cell.length_a
 cell.length_a
@@ -35,14 +35,14 @@ cell.angle_alpha
 cell.angle_beta
 cell.angle_beta
 cell.angle_gamma
 cell.angle_gamma
 cell.volume
 cell.volume
-cell.formula_units_Z
+cell.formula_units_z
 
 
 atom_site.label
 atom_site.label
 atom_site.type_symbol
 atom_site.type_symbol
 atom_site.fract_x
 atom_site.fract_x
 atom_site.fract_y
 atom_site.fract_y
 atom_site.fract_z
 atom_site.fract_z
-atom_site.U_iso_or_equiv
+atom_site.u_iso_or_equiv
 atom_site.adp_type
 atom_site.adp_type
 atom_site.occupancy
 atom_site.occupancy
 atom_site.calc_flag
 atom_site.calc_flag
@@ -52,20 +52,13 @@ atom_site.disorder_group
 atom_site.site_symmetry_multiplicity
 atom_site.site_symmetry_multiplicity
 
 
 atom_site_aniso.label
 atom_site_aniso.label
-atom_site_aniso.U
-atom_site_aniso.U_11
-atom_site_aniso.U_22
-atom_site_aniso.U_33
-atom_site_aniso.U_23
-atom_site_aniso.U_13
-atom_site_aniso.U_12
-atom_site_aniso.U_su
-atom_site_aniso.U_11_su
-atom_site_aniso.U_22_su
-atom_site_aniso.U_33_su
-atom_site_aniso.U_23_su
-atom_site_aniso.U_13_su
-atom_site_aniso.U_12_su
+atom_site_aniso.u
+atom_site_aniso.u_11
+atom_site_aniso.u_22
+atom_site_aniso.u_33
+atom_site_aniso.u_23
+atom_site_aniso.u_13
+atom_site_aniso.u_12
 
 
 geom_bond.atom_site_label_1
 geom_bond.atom_site_label_1
 geom_bond.atom_site_label_2
 geom_bond.atom_site_label_2

文件差异内容过多而无法显示
+ 462 - 280
package-lock.json


+ 55 - 50
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "molstar",
   "name": "molstar",
-  "version": "2.2.0",
+  "version": "3.0.0-dev.3",
   "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": {
@@ -16,6 +16,8 @@
     "test": "npm run lint && jest",
     "test": "npm run lint && jest",
     "jest": "jest",
     "jest": "jest",
     "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
     "build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
+    "clean": "node ./scripts/clean.js",
+    "rebuild": "npm run clean && npm run build",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
     "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
     "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
@@ -36,7 +38,7 @@
     "volume-server-test": "node lib/commonjs/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
     "volume-server-test": "node lib/commonjs/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
     "plugin-state": "node lib/commonjs/servers/plugin-state/index.js --working-folder ./build/state --port 1339",
     "plugin-state": "node lib/commonjs/servers/plugin-state/index.js --working-folder ./build/state --port 1339",
     "preversion": "npm run test",
     "preversion": "npm run test",
-    "version": "npm run build",
+    "version": "npm run rebuild && cpx .npmignore lib/",
     "postversion": "git push && git push --tags"
     "postversion": "git push && git push --tags"
   },
   },
   "files": [
   "files": [
@@ -88,68 +90,71 @@
   ],
   ],
   "license": "MIT",
   "license": "MIT",
   "devDependencies": {
   "devDependencies": {
-    "@graphql-codegen/add": "^2.0.2",
-    "@graphql-codegen/cli": "^1.19.4",
-    "@graphql-codegen/time": "^2.0.2",
-    "@graphql-codegen/typescript": "^1.19.0",
-    "@graphql-codegen/typescript-graphql-files-modules": "^1.18.1",
-    "@graphql-codegen/typescript-graphql-request": "^2.0.3",
-    "@graphql-codegen/typescript-operations": "^1.17.12",
-    "@types/cors": "^2.8.8",
-    "@typescript-eslint/eslint-plugin": "^4.9.1",
-    "@typescript-eslint/parser": "^4.9.1",
+    "@graphql-codegen/add": "^3.1.0",
+    "@graphql-codegen/cli": "^2.3.0",
+    "@graphql-codegen/time": "^3.1.0",
+    "@graphql-codegen/typescript": "^2.4.1",
+    "@graphql-codegen/typescript-graphql-files-modules": "^2.1.0",
+    "@graphql-codegen/typescript-graphql-request": "^4.3.1",
+    "@graphql-codegen/typescript-operations": "^2.2.1",
+    "@types/cors": "^2.8.12",
+    "@types/gl": "^4.1.0",
+    "@types/jest": "^27.0.3",
+    "@typescript-eslint/eslint-plugin": "^5.5.0",
+    "@typescript-eslint/parser": "^5.5.0",
     "benchmark": "^2.1.4",
     "benchmark": "^2.1.4",
-    "concurrently": "^5.3.0",
-    "cpx2": "^3.0.0",
+    "concurrently": "^6.4.0",
+    "cpx2": "^4.0.0",
     "crypto-browserify": "^3.12.0",
     "crypto-browserify": "^3.12.0",
-    "css-loader": "^5.0.1",
-    "eslint": "^7.15.0",
+    "css-loader": "^6.5.1",
+    "eslint": "^8.3.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": "^9.0.1",
-    "graphql": "^15.4.0",
-    "http-server": "^0.12.3",
-    "jest": "^26.6.3",
-    "mini-css-extract-plugin": "^1.3.2",
-    "node-sass": "^6.0.0",
+    "fs-extra": "^10.0.0",
+    "graphql": "^15.7.2",
+    "http-server": "^14.0.0",
+    "jest": "^27.3.1",
+    "mini-css-extract-plugin": "^2.4.5",
     "path-browserify": "^1.0.1",
     "path-browserify": "^1.0.1",
     "raw-loader": "^4.0.2",
     "raw-loader": "^4.0.2",
-    "sass-loader": "^11.1.1",
-    "simple-git": "^2.25.0",
+    "sass": "^1.43.5",
+    "sass-loader": "^12.3.0",
+    "simple-git": "^2.47.0",
     "stream-browserify": "^3.0.0",
     "stream-browserify": "^3.0.0",
-    "style-loader": "^2.0.0",
-    "ts-jest": "^26.4.4",
-    "typescript": "^4.2.4",
-    "webpack": "^5.37.1",
-    "webpack-cli": "^4.7.0",
-    "webpack-version-file-plugin": "^0.4.0"
+    "style-loader": "^3.3.1",
+    "ts-jest": "^27.0.7",
+    "typescript": "^4.5.2",
+    "webpack": "^5.64.4",
+    "webpack-cli": "^4.9.1"
   },
   },
   "dependencies": {
   "dependencies": {
-    "@types/argparse": "^1.0.38",
-    "@types/benchmark": "^2.1.0",
-    "@types/compression": "1.7.0",
-    "@types/express": "^4.17.9",
-    "@types/jest": "^26.0.18",
-    "@types/node": "^14.14.11",
-    "@types/node-fetch": "^2.5.7",
-    "@types/react": "^17.0.0",
-    "@types/react-dom": "^17.0.0",
-    "@types/swagger-ui-dist": "3.30.0",
-    "argparse": "^1.0.10",
+    "@types/argparse": "^2.0.10",
+    "@types/benchmark": "^2.1.1",
+    "@types/compression": "1.7.2",
+    "@types/express": "^4.17.13",
+    "@types/node": "^16.11.10",
+    "@types/node-fetch": "^2.5.12",
+    "@types/react": "^17.0.37",
+    "@types/react-dom": "^17.0.11",
+    "@types/swagger-ui-dist": "3.30.1",
+    "argparse": "^2.0.1",
     "body-parser": "^1.19.0",
     "body-parser": "^1.19.0",
     "compression": "^1.7.4",
     "compression": "^1.7.4",
     "cors": "^2.8.5",
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "express": "^4.17.1",
     "h264-mp4-encoder": "^1.0.12",
     "h264-mp4-encoder": "^1.0.12",
-    "immer": "^8.0.1",
+    "immer": "^9.0.7",
     "immutable": "^3.8.2",
     "immutable": "^3.8.2",
-    "node-fetch": "^2.6.1",
-    "react": "^17.0.1",
-    "react-dom": "^17.0.1",
-    "rxjs": "^6.6.6",
-    "swagger-ui-dist": "^3.37.2",
-    "tslib": "^2.1.0",
-    "util.promisify": "^1.0.1",
-    "xhr2": "^0.2.0"
+    "node-fetch": "^2.6.2",
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2",
+    "rxjs": "^7.4.0",
+    "swagger-ui-dist": "^4.1.1",
+    "tslib": "^2.3.1",
+    "util.promisify": "^1.1.1",
+    "xhr2": "^0.2.1"
+  },
+  "optionalDependencies": {
+    "gl": "^4.9.2"
   }
   }
 }
 }

+ 41 - 0
scripts/clean.js

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Michal Malý <malym@ibt.cas.cz>
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+function removeDir(dirPath) {
+    for (const ent of fs.readdirSync(dirPath)) {
+        const entryPath = path.join(dirPath, ent);
+        remove(entryPath);
+    }
+
+    fs.rmdirSync(dirPath);
+}
+
+function remove(entryPath) {
+    const st = fs.statSync(entryPath);
+    if (st.isDirectory())
+        removeDir(entryPath);
+    else
+        fs.unlinkSync(entryPath);
+}
+
+const toClean = [
+    path.resolve(__dirname, '../build'),
+    path.resolve(__dirname, '../lib'),
+    path.resolve(__dirname, '../tsconfig.tsbuildinfo'),
+];
+
+toClean.forEach(ph => {
+    if (fs.existsSync(ph)) {
+        try {
+            remove(ph);
+        } catch (err) {
+            console.warn(`Cleanup failed: ${err}`);
+        }
+    }
+});

+ 10 - 0
scripts/deploy.js

@@ -14,6 +14,9 @@ const buildDir = path.resolve(__dirname, '../build/');
 const deployDir = path.resolve(buildDir, 'deploy/');
 const deployDir = path.resolve(buildDir, 'deploy/');
 const localPath = path.resolve(deployDir, 'molstar.github.io/');
 const localPath = path.resolve(deployDir, 'molstar.github.io/');
 
 
+const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
+const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics -->`;
+
 function log(command, stdout, stderr) {
 function log(command, stdout, stderr) {
     if (command) {
     if (command) {
         console.log('\n###', command);
         console.log('\n###', command);
@@ -22,11 +25,18 @@ function log(command, stdout, stderr) {
     }
     }
 }
 }
 
 
+function addAnalytics(path) {
+    const data = fs.readFileSync(path, 'utf8');
+    const result = data.replace(analyticsTag, analyticsCode);
+    fs.writeFileSync(path, result, 'utf8');
+}
+
 function copyViewer() {
 function copyViewer() {
     console.log('\n###', 'copy viewer files');
     console.log('\n###', 'copy viewer files');
     const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
     const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
     const viewerDeployPath = path.resolve(localPath, 'viewer/');
     const viewerDeployPath = path.resolve(localPath, 'viewer/');
     fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
     fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
+    addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
 }
 }
 
 
 if (!fs.existsSync(localPath)) {
 if (!fs.existsSync(localPath)) {

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

@@ -21,12 +21,12 @@ import { PluginContext } from '../../mol-plugin/context';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { StateObjectRef } from '../../mol-state';
 import { StateObjectRef } from '../../mol-state';
 import { Color } from '../../mol-util/color';
 import { Color } from '../../mol-util/color';
+import { Material } from '../../mol-util/material';
 
 
 function shinyStyle(plugin: PluginContext) {
 function shinyStyle(plugin: PluginContext) {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
         renderer: {
         renderer: {
             ...plugin.canvas3d!.props.renderer,
             ...plugin.canvas3d!.props.renderer,
-            style: { name: 'plastic', params: {} },
         },
         },
         postprocessing: {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             ...plugin.canvas3d!.props.postprocessing,
@@ -40,7 +40,6 @@ function occlusionStyle(plugin: PluginContext) {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
         renderer: {
         renderer: {
             ...plugin.canvas3d!.props.renderer,
             ...plugin.canvas3d!.props.renderer,
-            style: { name: 'flat', params: {} }
         },
         },
         postprocessing: {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             ...plugin.canvas3d!.props.postprocessing,
@@ -52,7 +51,8 @@ function occlusionStyle(plugin: PluginContext) {
             } },
             } },
             outline: { name: 'on', params: {
             outline: { name: 'on', params: {
                 scale: 1.0,
                 scale: 1.0,
-                threshold: 0.33
+                threshold: 0.33,
+                color: Color(0x0000),
             } }
             } }
         }
         }
     } });
     } });
@@ -77,7 +77,7 @@ const PresetParams = {
     ...StructureRepresentationPresetProvider.CommonParams,
     ...StructureRepresentationPresetProvider.CommonParams,
 };
 };
 
 
-
+const CustomMaterial = Material({ roughness: 0.2, metalness: 0 });
 
 
 export const StructurePreset = StructureRepresentationPresetProvider({
 export const StructurePreset = StructureRepresentationPresetProvider({
     id: 'preset-structure',
     id: 'preset-structure',
@@ -94,8 +94,8 @@ export const StructurePreset = StructureRepresentationPresetProvider({
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.35 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color: 'chain-id', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.35 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, material: CustomMaterial }, color: 'chain-id', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -121,8 +121,8 @@ export const IllustrativePreset = StructureRepresentationPresetProvider({
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'spacefill', typeParams: { ...typeParams }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'spacefill', typeParams: { ...typeParams, ignoreLight: true }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'spacefill', typeParams: { ...typeParams, ignoreLight: true }, color: 'illustrative', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -149,8 +149,8 @@ const SurfacePreset = StructureRepresentationPresetProvider({
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'molecular-surface', typeParams: { ...typeParams, quality: 'custom', resolution: 0.5, doubleSided: true }, color: 'partial-charge' }, { tag: 'polymer' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'molecular-surface', typeParams: { ...typeParams, material: CustomMaterial, quality: 'custom', resolution: 0.5, doubleSided: true }, color: 'partial-charge' }, { tag: 'polymer' }),
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -177,8 +177,8 @@ const PocketPreset = StructureRepresentationPresetProvider({
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
-            surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
+            surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, material: CustomMaterial, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }),
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -206,10 +206,10 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
-            ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
-            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
-            label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
+            ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
+            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
+            label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
         };
         };
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
@@ -238,7 +238,7 @@ export class ViewportComponent extends PluginUIComponent {
     pocketPreset = () => this.set(PocketPreset);
     pocketPreset = () => this.set(PocketPreset);
     interactionsPreset = () => this.set(InteractionsPreset);
     interactionsPreset = () => this.set(InteractionsPreset);
 
 
-    get showButtons () {
+    get showButtons() {
         return this.plugin.config.get(ShowButtons);
         return this.plugin.config.get(ShowButtons);
     }
     }
 
 

+ 1 - 1
src/apps/viewer/embedded.html

@@ -36,7 +36,7 @@
                 emdbProvider: 'rcsb',
                 emdbProvider: 'rcsb',
             });
             });
             viewer.loadPdb('7bv2');
             viewer.loadPdb('7bv2');
-            viewer.loadEmdb('EMD-30210', { detail: 6 });
+            viewer.loadEmdb('EMD-30210', { detail: 6 });            
 
 
             // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
             // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
         </script>
         </script>

+ 11 - 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();
@@ -81,5 +91,6 @@
             var emdb = getParam('emdb', '[^&]+').trim();
             var emdb = getParam('emdb', '[^&]+').trim();
             if (emdb) viewer.loadEmdb(emdb);
             if (emdb) viewer.loadEmdb(emdb);
         </script>
         </script>
+        <!-- __MOLSTAR_ANALYTICS__ -->
     </body>
     </body>
 </html>
 </html>

+ 78 - 8
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>
@@ -9,25 +9,28 @@ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
 import { CellPack } from '../../extensions/cellpack';
 import { CellPack } from '../../extensions/cellpack';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
-import { Mp4Export } from '../../extensions/mp4-export';
 import { GeometryExport } from '../../extensions/geo-export';
 import { GeometryExport } from '../../extensions/geo-export';
+import { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
 import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
+import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
 import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
 import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
 import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
 import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
+import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure';
 import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
 import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
 import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
 import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
 import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
 import { createPlugin } from '../../mol-plugin-ui';
 import { createPlugin } from '../../mol-plugin-ui';
 import { PluginUIContext } from '../../mol-plugin-ui/context';
 import { PluginUIContext } from '../../mol-plugin-ui/context';
-import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
 import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginConfig } from '../../mol-plugin/config';
 import { PluginConfig } from '../../mol-plugin/config';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { PluginSpec } from '../../mol-plugin/spec';
 import { PluginSpec } from '../../mol-plugin/spec';
 import { PluginState } from '../../mol-plugin/state';
 import { PluginState } from '../../mol-plugin/state';
 import { StateObjectSelector } from '../../mol-state';
 import { StateObjectSelector } from '../../mol-state';
@@ -71,9 +74,12 @@ const DefaultViewerOptions = {
     layoutShowLog: true,
     layoutShowLog: true,
     layoutShowLeftPanel: true,
     layoutShowLeftPanel: true,
     collapseLeftPanel: false,
     collapseLeftPanel: false,
-    disableAntialiasing: false,
-    pixelScale: 1,
-    enableWboit: true,
+    collapseRightPanel: false,
+    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,
@@ -112,7 +118,7 @@ export class Viewer {
                     regionState: {
                     regionState: {
                         bottom: 'full',
                         bottom: 'full',
                         left: o.collapseLeftPanel ? 'collapsed' : 'full',
                         left: o.collapseLeftPanel ? 'collapsed' : 'full',
-                        right: 'full',
+                        right: o.collapseRightPanel ? 'hidden' : 'full',
                         top: 'full',
                         top: 'full',
                     }
                     }
                 },
                 },
@@ -130,6 +136,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],
@@ -324,8 +332,58 @@ export class Viewer {
         });
         });
     }
     }
 
 
+    /**
+     * @example
+     *  viewer.loadTrajectory({
+     *      model: { kind: 'model-url', url: 'villin.gro', format: 'gro' },
+     *      coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true },
+     *      preset: 'all-models' // or 'default'
+     *  });
+     */
+    async loadTrajectory(params: LoadTrajectoryParams) {
+        const plugin = this.plugin;
+
+        let model: StateObjectSelector, coords: StateObjectSelector;
+
+        if (params.model.kind === 'model-data' || params.model.kind === 'model-url') {
+            const data = params.model.kind === 'model-data'
+                ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
+                : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
+
+            const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif');
+            model = await plugin.builders.structure.createModel(trajectory);
+        } else {
+            const data = params.model.kind === 'topology-data'
+                ? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
+                : await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
+
+            const provider = plugin.dataFormats.get(params.model.format);
+            model = await provider!.parse(plugin, data);
+        }
+
+        {
+            const data = params.coordinates.kind === 'coordinates-data'
+                ? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel })
+                : await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel });
+
+            const provider = plugin.dataFormats.get(params.coordinates.format);
+            coords = await provider!.parse(plugin, data);
+        }
+
+        const trajectory = await plugin.build().toRoot()
+            .apply(TrajectoryFromModelAndCoordinates, {
+                modelRef: model.ref,
+                coordinatesRef: coords.ref
+            }, { dependsOn: [model.ref, coords.ref] })
+            .commit();
+
+        const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default');
+
+        return { model, coords, preset };
+    }
+
     handleResize() {
     handleResize() {
-        this.plugin.layout.events.updated.next();
+        this.plugin.layout.events.updated.next(void 0);
     }
     }
 }
 }
 
 
@@ -339,4 +397,16 @@ export interface VolumeIsovalueInfo {
     color: Color,
     color: Color,
     alpha?: number,
     alpha?: number,
     volumeIndex?: number
     volumeIndex?: number
+}
+
+export interface LoadTrajectoryParams {
+    model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
+    | { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
+    | { kind: 'topology-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
+    | { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
+    modelLabel?: string,
+    coordinates: { kind: 'coordinates-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
+    | { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
+    coordinatesLabel?: string,
+    preset?: keyof PresetTrajectoryHierarchy
 }
 }

+ 6 - 6
src/cli/chem-comp-dict/create-ions.ts

@@ -19,7 +19,7 @@ import { ensureDataAvailable, readCCD } from './util';
 function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
 function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
     const ionNames: string[] = [];
     const ionNames: string[] = [];
     for (const k in ccd) {
     for (const k in ccd) {
-        const {chem_comp} = ccd[k];
+        const { chem_comp } = ccd[k];
         if (chem_comp.name.value(0).toUpperCase().includes(' ION')) {
         if (chem_comp.name.value(0).toUpperCase().includes(' ION')) {
             ionNames.push(chem_comp.id.value(0));
             ionNames.push(chem_comp.id.value(0));
         }
         }
@@ -54,20 +54,20 @@ async function run(out: string, forceDownload = false) {
 }
 }
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Extract and save IonNames from CCD.'
     description: 'Extract and save IonNames from CCD.'
 });
 });
-parser.addArgument('out', {
+parser.add_argument('out', {
     help: 'Generated file output path.'
     help: 'Generated file output path.'
 });
 });
-parser.addArgument([ '--forceDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDownload', '-f', {
+    action: 'store_true',
     help: 'Force download of CCD and PVCD.'
     help: 'Force download of CCD and PVCD.'
 });
 });
 interface Args {
 interface Args {
     out: string,
     out: string,
     forceDownload?: boolean,
     forceDownload?: boolean,
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 run(args.out, args.forceDownload);
 run(args.out, args.forceDownload);

+ 9 - 9
src/cli/chem-comp-dict/create-table.ts

@@ -171,7 +171,7 @@ async function createBonds(
         pdbx_aromatic_flag, pdbx_stereo_config, molstar_protonation_variant
         pdbx_aromatic_flag, pdbx_stereo_config, molstar_protonation_variant
     });
     });
 
 
-    const bondDatabase =  Database.ofTables(
+    const bondDatabase = Database.ofTables(
         CCB_TABLE_NAME,
         CCB_TABLE_NAME,
         { chem_comp_bond: mmCIF_chemCompBond_schema },
         { chem_comp_bond: mmCIF_chemCompBond_schema },
         { chem_comp_bond: bondTable }
         { chem_comp_bond: bondTable }
@@ -265,21 +265,21 @@ const CCB_TABLE_NAME = 'CHEM_COMP_BONDS';
 const CCA_TABLE_NAME = 'CHEM_COMP_ATOMS';
 const CCA_TABLE_NAME = 'CHEM_COMP_ATOMS';
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Create a cif file with one big table of all chem_comp_bond entries from the CCD and PVCD.'
     description: 'Create a cif file with one big table of all chem_comp_bond entries from the CCD and PVCD.'
 });
 });
-parser.addArgument('out', {
+parser.add_argument('out', {
     help: 'Generated file output path.'
     help: 'Generated file output path.'
 });
 });
-parser.addArgument([ '--forceDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDownload', '-f', {
+    action: 'store_true',
     help: 'Force download of CCD and PVCD.'
     help: 'Force download of CCD and PVCD.'
 });
 });
-parser.addArgument([ '--binary', '-b' ], {
-    action: 'storeTrue',
+parser.add_argument('--binary', '-b', {
+    action: 'store_true',
     help: 'Output as BinaryCIF.'
     help: 'Output as BinaryCIF.'
 });
 });
-parser.addArgument(['--ccaOut', '-a'], {
+parser.add_argument('--ccaOut', '-a', {
     help: 'Optional generated file output path for chem_comp_atom data.',
     help: 'Optional generated file output path for chem_comp_atom data.',
     required: false
     required: false
 });
 });
@@ -289,6 +289,6 @@ interface Args {
     binary?: boolean,
     binary?: boolean,
     ccaOut?: string
     ccaOut?: string
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 run(args.out, args.binary, args.forceDownload, args.ccaOut);
 run(args.out, args.binary, args.forceDownload, args.ccaOut);

+ 6 - 6
src/cli/cif2bcif/index.ts

@@ -37,20 +37,20 @@ function run(args: Args) {
 }
 }
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Convert any CIF file to a BCIF file'
     description: 'Convert any CIF file to a BCIF file'
 });
 });
-parser.addArgument([ 'src' ], {
+parser.add_argument('src', {
     help: 'Source CIF path'
     help: 'Source CIF path'
 });
 });
-parser.addArgument([ 'out' ], {
+parser.add_argument('out', {
     help: 'Output BCIF path'
     help: 'Output BCIF path'
 });
 });
-parser.addArgument([ '-c', '--config' ], {
+parser.add_argument('-c', '--config', {
     help: 'Optional encoding strategy/precision config path',
     help: 'Optional encoding strategy/precision config path',
     required: false
     required: false
 });
 });
-parser.addArgument([ '-f', '--filter' ], {
+parser.add_argument('-f', '--filter', {
     help: 'Optional filter whitelist/blacklist path',
     help: 'Optional filter whitelist/blacklist path',
     required: false
     required: false
 });
 });
@@ -61,7 +61,7 @@ interface Args {
     config?: string
     config?: string
     filter?: string
     filter?: string
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 if (args) {
 if (args) {
     run(args);
     run(args);

+ 23 - 23
src/cli/cifschema/index.ts

@@ -124,15 +124,15 @@ async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> {
     const csvFile = parsed.result;
     const csvFile = parsed.result;
 
 
     const fieldNamesCol = csvFile.table.getColumn('0');
     const fieldNamesCol = csvFile.table.getColumn('0');
-    if (!fieldNamesCol) throw 'error getting fields columns';
+    if (!fieldNamesCol) throw new Error('error getting fields columns');
     const fieldNames = fieldNamesCol.toStringArray();
     const fieldNames = fieldNamesCol.toStringArray();
 
 
     const filter: Filter = {};
     const filter: Filter = {};
     fieldNames.forEach((name, i) => {
     fieldNames.forEach((name, i) => {
-        const [ category, field ] = name.split('.');
+        const [category, field] = name.split('.');
         // console.log(category, field)
         // console.log(category, field)
-        if (!filter[ category ]) filter[ category ] = {};
-        filter[ category ][ field ] = true;
+        if (!filter[category]) filter[category] = {};
+        filter[category][field] = true;
     });
     });
     return filter;
     return filter;
 }
 }
@@ -178,44 +178,44 @@ const CIF_CORE_ATTR_PATH = `${DIC_DIR}/templ_attr.cif`;
 const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_attr.cif';
 const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_attr.cif';
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Create schema from mmcif dictionary (v50 plus IHM and entity_branch extensions, downloaded from wwPDB)'
     description: 'Create schema from mmcif dictionary (v50 plus IHM and entity_branch extensions, downloaded from wwPDB)'
 });
 });
-parser.addArgument([ '--preset', '-p' ], {
-    defaultValue: '',
+parser.add_argument('--preset', '-p', {
+    default: '',
     choices: ['', 'mmCIF', 'CCD', 'BIRD', 'CifCore'],
     choices: ['', 'mmCIF', 'CCD', 'BIRD', 'CifCore'],
     help: 'Preset name'
     help: 'Preset name'
 });
 });
-parser.addArgument([ '--name', '-n' ], {
-    defaultValue: '',
+parser.add_argument('--name', '-n', {
+    default: '',
     help: 'Schema name'
     help: 'Schema name'
 });
 });
-parser.addArgument([ '--out', '-o' ], {
+parser.add_argument('--out', '-o', {
     help: 'Generated schema output path, if not given printed to stdout'
     help: 'Generated schema output path, if not given printed to stdout'
 });
 });
-parser.addArgument([ '--targetFormat', '-tf' ], {
-    defaultValue: 'typescript-molstar',
+parser.add_argument('--targetFormat', '-tf', {
+    default: 'typescript-molstar',
     choices: ['typescript-molstar', 'json-internal'],
     choices: ['typescript-molstar', 'json-internal'],
     help: 'Target format'
     help: 'Target format'
 });
 });
-parser.addArgument([ '--dicPath', '-d' ], {
-    defaultValue: '',
+parser.add_argument('--dicPath', '-d', {
+    default: '',
     help: 'Path to dictionary'
     help: 'Path to dictionary'
 });
 });
-parser.addArgument([ '--fieldNamesPath', '-fn' ], {
-    defaultValue: '',
+parser.add_argument('--fieldNamesPath', '-fn', {
+    default: '',
     help: 'Field names to include'
     help: 'Field names to include'
 });
 });
-parser.addArgument([ '--forceDicDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDicDownload', '-f', {
+    action: 'store_true',
     help: 'Force download of dictionaries'
     help: 'Force download of dictionaries'
 });
 });
-parser.addArgument([ '--moldataImportPath', '-mip' ], {
-    defaultValue: 'molstar/lib/mol-data',
+parser.add_argument('--moldataImportPath', '-mip', {
+    default: 'molstar/lib/mol-data',
     help: 'mol-data import path (for typescript target only)'
     help: 'mol-data import path (for typescript target only)'
 });
 });
-parser.addArgument([ '--addAliases', '-aa' ], {
-    action: 'storeTrue',
+parser.add_argument('--addAliases', '-aa', {
+    action: 'store_true',
     help: 'Add field name/path aliases'
     help: 'Add field name/path aliases'
 });
 });
 interface Args {
 interface Args {
@@ -230,7 +230,7 @@ interface Args {
     moldataImportPath: string
     moldataImportPath: string
     addAliases: boolean
     addAliases: boolean
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 const FORCE_DIC_DOWNLOAD = args.forceDicDownload;
 const FORCE_DIC_DOWNLOAD = args.forceDicDownload;
 
 

+ 22 - 22
src/cli/cifschema/util/cif-dic.ts

@@ -34,6 +34,8 @@ export function getFieldType(type: string, description: string, values?: string[
         case 'seq-one-letter-code':
         case 'seq-one-letter-code':
         case 'author':
         case 'author':
         case 'orcid_id':
         case 'orcid_id':
+        case 'pdbx_PDB_obsoleted_db_id':
+        case 'pdbx_related_db_id':
         case 'sequence_dep':
         case 'sequence_dep':
         case 'pdb_id':
         case 'pdb_id':
         case 'emd_id':
         case 'emd_id':
@@ -79,9 +81,10 @@ export function getFieldType(type: string, description: string, values?: string[
         case 'List(Real,Real)':
         case 'List(Real,Real)':
         case 'List(Real,Real,Real,Real)':
         case 'List(Real,Real,Real,Real)':
         case 'Date':
         case 'Date':
-        case 'Datetime':
+        case 'DateTime':
         case 'Tag':
         case 'Tag':
         case 'Implied':
         case 'Implied':
+        case 'Word':
             return wrapContainer('str', ',', description, container);
             return wrapContainer('str', ',', description, container);
         case 'Real':
         case 'Real':
             return wrapContainer('float', ',', description, container);
             return wrapContainer('float', ',', description, container);
@@ -187,7 +190,7 @@ function getContainer(d: Data.CifFrame, imports: Imports, ctx: FrameData) {
 function getCode(d: Data.CifFrame, imports: Imports, ctx: FrameData): [string, string[] | undefined, string | undefined ] | undefined {
 function getCode(d: Data.CifFrame, imports: Imports, ctx: FrameData): [string, string[] | undefined, string | undefined ] | undefined {
     const code = getField('item_type', 'code', d, imports, ctx) || getField('type', 'contents', d, imports, ctx);
     const code = getField('item_type', 'code', d, imports, ctx) || getField('type', 'contents', d, imports, ctx);
     if (code) {
     if (code) {
-        return [ code.str(0), getEnums(d, imports, ctx), getContainer(d, imports, ctx) ];
+        return [code.str(0), getEnums(d, imports, ctx), getContainer(d, imports, ctx)];
     } else {
     } else {
         console.log(`item_type.code or type.contents not found for '${d.header}'`);
         console.log(`item_type.code or type.contents not found for '${d.header}'`);
     }
     }
@@ -232,29 +235,26 @@ const FORCE_INT_FIELDS = [
     '_struct_sheet_range.end_auth_seq_id',
     '_struct_sheet_range.end_auth_seq_id',
 ];
 ];
 
 
+/**
+ * Note that name and mapped name must share a prefix. This is not always the case in
+ * the cifCore dictionary, but for downstream code to work a container field with the
+ * same prefix as the member fields must be given here and in the field names filter
+ * list.
+ */
 const FORCE_MATRIX_FIELDS_MAP: { [k: string]: string } = {
 const FORCE_MATRIX_FIELDS_MAP: { [k: string]: string } = {
-    'atom_site_aniso.U_11': 'U',
-    'atom_site_aniso.U_22': 'U',
-    'atom_site_aniso.U_33': 'U',
-    'atom_site_aniso.U_23': 'U',
-    'atom_site_aniso.U_13': 'U',
-    'atom_site_aniso.U_12': 'U',
-    'atom_site_aniso.U_11_su': 'U_su',
-    'atom_site_aniso.U_22_su': 'U_su',
-    'atom_site_aniso.U_33_su': 'U_su',
-    'atom_site_aniso.U_23_su': 'U_su',
-    'atom_site_aniso.U_13_su': 'U_su',
-    'atom_site_aniso.U_12_su': 'U_su',
+    'atom_site_aniso.u_11': 'u', // is matrix_u in the the dic
+    'atom_site_aniso.u_22': 'u',
+    'atom_site_aniso.u_33': 'u',
+    'atom_site_aniso.u_23': 'u',
+    'atom_site_aniso.u_13': 'u',
+    'atom_site_aniso.u_12': 'u',
 };
 };
 const FORCE_MATRIX_FIELDS = Object.keys(FORCE_MATRIX_FIELDS_MAP);
 const FORCE_MATRIX_FIELDS = Object.keys(FORCE_MATRIX_FIELDS_MAP);
 
 
 const EXTRA_ALIASES: Database['aliases'] = {
 const EXTRA_ALIASES: Database['aliases'] = {
-    'atom_site_aniso.U': [
-        'atom_site_anisotrop_U'
-    ],
-    'atom_site_aniso.U_su': [
-        'atom_site_aniso_U_esd',
-        'atom_site_anisotrop_U_esd',
+    'atom_site_aniso.matrix_u': [
+        'atom_site_anisotrop_U',
+        'atom_site_aniso.U'
     ],
     ],
 };
 };
 
 
@@ -317,7 +317,7 @@ export function generateSchema(frames: CifFrame[], imports: Imports = new Map())
     frames.forEach(d => {
     frames.forEach(d => {
         // category definitions in mmCIF start with '_' and don't include a '.'
         // category definitions in mmCIF start with '_' and don't include a '.'
         // category definitions in cifCore don't include a '.'
         // category definitions in cifCore don't include a '.'
-        if (d.header[0] === '_'  || d.header.includes('.')) return;
+        if (d.header[0] === '_' || d.header.includes('.')) return;
         const categoryName = d.header.toLowerCase();
         const categoryName = d.header.toLowerCase();
         // console.log(d.header, d.categoryNames, d.categories)
         // console.log(d.header, d.categoryNames, d.categories)
         let descriptionField: Data.CifField | undefined;
         let descriptionField: Data.CifField | undefined;
@@ -372,7 +372,7 @@ export function generateSchema(frames: CifFrame[], imports: Imports = new Map())
             const parent_name = item_linked.getField('parent_name');
             const parent_name = item_linked.getField('parent_name');
             if (child_name && parent_name) {
             if (child_name && parent_name) {
                 for (let i = 0; i < item_linked.rowCount; ++i) {
                 for (let i = 0; i < item_linked.rowCount; ++i) {
-                    const childName = child_name.str(i);
+                    const childName: string = child_name.str(i);
                     const parentName = parent_name.str(i);
                     const parentName = parent_name.str(i);
                     if (childName in links && links[childName] !== parentName) {
                     if (childName in links && links[childName] !== parentName) {
                         console.log(`${childName} linked to ${links[childName]}, ignoring link to ${parentName}`);
                         console.log(`${childName} linked to ${links[childName]}, ignoring link to ${parentName}`);

+ 4 - 4
src/cli/cifschema/util/generate.ts

@@ -8,7 +8,7 @@ import { Database, Filter, Column } from './schema';
 import { indentString } from '../../../mol-util/string';
 import { indentString } from '../../../mol-util/string';
 import { FieldPath } from '../../../mol-io/reader/cif/schema';
 import { FieldPath } from '../../../mol-io/reader/cif/schema';
 
 
-function header (name: string, info: string, moldataImportPath: string) {
+function header(name: string, info: string, moldataImportPath: string) {
     return `/**
     return `/**
  * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
@@ -22,7 +22,7 @@ import { Database, Column } from '${moldataImportPath}/db';
 import Schema = Column.Schema;`;
 import Schema = Column.Schema;`;
 }
 }
 
 
-function footer (name: string) {
+function footer(name: string) {
     return `
     return `
 export type ${name}_Schema = typeof ${name}_Schema;
 export type ${name}_Schema = typeof ${name}_Schema;
 export interface ${name}_Database extends Database<${name}_Schema> {};`;
 export interface ${name}_Database extends Database<${name}_Schema> {};`;
@@ -89,7 +89,7 @@ function doc(description: string, spacesCount: number) {
     ].join('\n');
     ].join('\n');
 }
 }
 
 
-export function generate (name: string, info: string, schema: Database, fields: Filter | undefined, moldataImportPath: string, addAliases: boolean) {
+export function generate(name: string, info: string, schema: Database, fields: Filter | undefined, moldataImportPath: string, addAliases: boolean) {
     const codeLines: string[] = [];
     const codeLines: string[] = [];
 
 
     if (fields) {
     if (fields) {
@@ -128,7 +128,7 @@ export function generate (name: string, info: string, schema: Database, fields:
         codeLines.push('');
         codeLines.push('');
         codeLines.push(`export const ${name}_Aliases = {`);
         codeLines.push(`export const ${name}_Aliases = {`);
         Object.keys(schema.aliases).forEach(path => {
         Object.keys(schema.aliases).forEach(path => {
-            const [ table, columnName ] = path.split('.');
+            const [table, columnName] = path.split('.');
             if (fields && !fields[table]) return;
             if (fields && !fields[table]) return;
             if (fields && !fields[table][columnName]) return;
             if (fields && !fields[table][columnName]) return;
 
 

+ 2 - 2
src/cli/cifschema/util/helper.ts

@@ -10,8 +10,8 @@ export function parseImportGet(s: string): Import[] {
     // [{'save':hi_ang_Fox_coeffs  'file':templ_attr.cif}   {'save':hi_ang_Fox_c0  'file':templ_enum.cif}]
     // [{'save':hi_ang_Fox_coeffs  'file':templ_attr.cif}   {'save':hi_ang_Fox_c0  'file':templ_enum.cif}]
     // [{"file":'templ_enum.cif' "save":'H_M_ref'}]
     // [{"file":'templ_enum.cif' "save":'H_M_ref'}]
     return s.trim().substring(2, s.length - 2).split(/}[ \n\t]*{/g).map(s => {
     return s.trim().substring(2, s.length - 2).split(/}[ \n\t]*{/g).map(s => {
-        const save = s.match(/('save'|"save"):([^ \t\n]+)/);
-        const file = s.match(/('file'|"file"):([^ \t\n]+)/);
+        const save = s.match(/('save'|"save"):([^ \t\n{}]+)/);
+        const file = s.match(/('file'|"file"):([^ \t\n{}]+)/);
         return {
         return {
             save: save ? save[0].substr(7).replace(/['"]/g, '') : undefined,
             save: save ? save[0].substr(7).replace(/['"]/g, '') : undefined,
             file: file ? file[0].substr(7).replace(/['"]/g, '') : undefined
             file: file ? file[0].substr(7).replace(/['"]/g, '') : undefined

+ 2 - 2
src/cli/cifschema/util/schema.ts

@@ -51,13 +51,13 @@ export function ListCol(subType: 'int' | 'str' | 'float' | 'coord', separator: s
 
 
 export type Filter = { [ table: string ]: { [ column: string ]: true } }
 export type Filter = { [ table: string ]: { [ column: string ]: true } }
 
 
-export function mergeFilters (...filters: Filter[]) {
+export function mergeFilters(...filters: Filter[]) {
     const n = filters.length;
     const n = filters.length;
     const mergedFilter: Filter = {};
     const mergedFilter: Filter = {};
     const fields: Map<string, number> = new Map();
     const fields: Map<string, number> = new Map();
     filters.forEach(filter => {
     filters.forEach(filter => {
         Object.keys(filter).forEach(category => {
         Object.keys(filter).forEach(category => {
-            Object.keys(filter[ category ]).forEach(field => {
+            Object.keys(filter[category]).forEach(field => {
                 const key = `${category}.${field}`;
                 const key = `${category}.${field}`;
                 const value = fields.get(key) || 0;
                 const value = fields.get(key) || 0;
                 fields.set(key, value + 1);
                 fields.set(key, value + 1);

+ 5 - 5
src/cli/lipid-params/index.ts

@@ -70,21 +70,21 @@ export const LipidNames = new Set(${lipidNames.replace(/"/g, "'").replace(/,/g,
 }
 }
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Create lipid params (from martini lipids itp)'
     description: 'Create lipid params (from martini lipids itp)'
 });
 });
-parser.addArgument([ '--out', '-o' ], {
+parser.add_argument('--out', '-o', {
     help: 'Generated lipid params output path, if not given printed to stdout'
     help: 'Generated lipid params output path, if not given printed to stdout'
 });
 });
-parser.addArgument([ '--forceDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDownload', '-f', {
+    action: 'store_true',
     help: 'Force download of martini lipids itp'
     help: 'Force download of martini lipids itp'
 });
 });
 interface Args {
 interface Args {
     out: string
     out: string
     forceDownload: boolean
     forceDownload: boolean
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 const FORCE_DOWNLOAD = args.forceDownload;
 const FORCE_DOWNLOAD = args.forceDownload;
 
 

+ 15 - 15
src/cli/structure-info/model.ts

@@ -63,7 +63,7 @@ export function printSecStructure(model: Model) {
     const count = residues._rowCount;
     const count = residues._rowCount;
     let rI = 0;
     let rI = 0;
     while (rI < count) {
     while (rI < count) {
-        let start = rI;
+        const start = rI;
         while (rI < count && key[start] === key[rI]) rI++;
         while (rI < count && key[start] === key[rI]) rI++;
         rI--;
         rI--;
 
 
@@ -230,21 +230,21 @@ async function runFile(filename: string, args: Args) {
 }
 }
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Print info about a structure, mainly to test and showcase the mol-model module'
     description: 'Print info about a structure, mainly to test and showcase the mol-model module'
 });
 });
-parser.addArgument(['--download', '-d'], { help: 'Pdb entry id' });
-parser.addArgument(['--file', '-f'], { help: 'filename' });
-
-parser.addArgument(['--models'], { help: 'print models info', action: 'storeTrue' });
-parser.addArgument(['--seq'], { help: 'print sequence', action: 'storeTrue' });
-parser.addArgument(['--units'], { help: 'print units', action: 'storeTrue' });
-parser.addArgument(['--sym'], { help: 'print symmetry', action: 'storeTrue' });
-parser.addArgument(['--rings'], { help: 'print rings', action: 'storeTrue' });
-parser.addArgument(['--intraBonds'], { help: 'print intra unit bonds', action: 'storeTrue' });
-parser.addArgument(['--interBonds'], { help: 'print inter unit bonds', action: 'storeTrue' });
-parser.addArgument(['--mod'], { help: 'print modified residues', action: 'storeTrue' });
-parser.addArgument(['--sec'], { help: 'print secoundary structure', action: 'storeTrue' });
+parser.add_argument('--download', '-d', { help: 'Pdb entry id' });
+parser.add_argument('--file', '-f', { help: 'filename' });
+
+parser.add_argument('--models', { help: 'print models info', action: 'store_true' });
+parser.add_argument('--seq', { help: 'print sequence', action: 'store_true' });
+parser.add_argument('--units', { help: 'print units', action: 'store_true' });
+parser.add_argument('--sym', { help: 'print symmetry', action: 'store_true' });
+parser.add_argument('--rings', { help: 'print rings', action: 'store_true' });
+parser.add_argument('--intraBonds', { help: 'print intra unit bonds', action: 'store_true' });
+parser.add_argument('--interBonds', { help: 'print inter unit bonds', action: 'store_true' });
+parser.add_argument('--mod', { help: 'print modified residues', action: 'store_true' });
+parser.add_argument('--sec', { help: 'print secoundary structure', action: 'store_true' });
 interface Args {
 interface Args {
     download?: string,
     download?: string,
     file?: string,
     file?: string,
@@ -260,7 +260,7 @@ interface Args {
     mod?: boolean,
     mod?: boolean,
     sec?: boolean,
     sec?: boolean,
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 if (args.download) runDL(args.download, args);
 if (args.download) runDL(args.download, args);
 else if (args.file) runFile(args.file, args);
 else if (args.file) runFile(args.file, args);

+ 5 - 5
src/cli/structure-info/volume.ts

@@ -38,7 +38,7 @@ function print(volume: Volume) {
 }
 }
 
 
 async function doMesh(volume: Volume, filename: string) {
 async function doMesh(volume: Volume, filename: string) {
-    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) } )).run();
+    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
 
     // Export the mesh in OBJ format.
     // Export the mesh in OBJ format.
@@ -75,13 +75,13 @@ async function run(url: string, meshFilename: string) {
 }
 }
 
 
 const parser = new argparse.ArgumentParser({
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Info about VolumeData from mol-model module'
     description: 'Info about VolumeData from mol-model module'
 });
 });
-parser.addArgument([ '--emdb', '-e' ], {
+parser.add_argument('--emdb', '-e', {
     help: 'EMDB id, for example 8116',
     help: 'EMDB id, for example 8116',
 });
 });
-parser.addArgument([ '--mesh' ], {
+parser.add_argument('--mesh', {
     help: 'Mesh filename',
     help: 'Mesh filename',
     required: true
     required: true
 });
 });
@@ -89,6 +89,6 @@ interface Args {
     emdb?: string,
     emdb?: string,
     mesh: string
     mesh: string
 }
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 
 run(`https://ds.litemol.org/em/emd-${args.emdb}/cell?detail=4`, args.mesh);
 run(`https://ds.litemol.org/em/emd-${args.emdb}/cell?detail=4`, args.mesh);

+ 1 - 1
src/examples/domain-annotation-server/mapping.ts

@@ -38,7 +38,7 @@ type MappingRow = Table.Row<S.mapping>;
 
 
 function writeDomain(enc: CifWriter.Encoder, domain: DomainAnnotation | undefined) {
 function writeDomain(enc: CifWriter.Encoder, domain: DomainAnnotation | undefined) {
     if (!domain) return;
     if (!domain) return;
-    enc.writeCategory({ name: `pdbx_${domain.name}_domain_annotation`, instance: () =>  CifWriter.Category.ofTable(domain.domains) });
+    enc.writeCategory({ name: `pdbx_${domain.name}_domain_annotation`, instance: () => CifWriter.Category.ofTable(domain.domains) });
     enc.writeCategory({ name: `pdbx_${domain.name}_domain_mapping`, instance: () => CifWriter.Category.ofTable(domain.mappings) });
     enc.writeCategory({ name: `pdbx_${domain.name}_domain_mapping`, instance: () => CifWriter.Category.ofTable(domain.mappings) });
 }
 }
 
 

+ 1 - 1
src/examples/domain-annotation-server/server.ts

@@ -15,7 +15,7 @@ async function getMappings(id: string) {
 };
 };
 
 
 
 
-let PORT = process.env.port || 1338;
+const PORT = process.env.port || 1338;
 
 
 const app = express();
 const app = express();
 
 

+ 3 - 2
src/examples/lighting/index.ts

@@ -11,6 +11,7 @@ import { PluginUIContext } from '../../mol-plugin-ui/context';
 import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
 import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { Asset } from '../../mol-util/assets';
 import { Asset } from '../../mol-util/assets';
+import { Color } from '../../mol-util/color';
 import './index.html';
 import './index.html';
 require('mol-plugin-ui/skin/light.scss');
 require('mol-plugin-ui/skin/light.scss');
 
 
@@ -26,7 +27,7 @@ const Canvas3DPresets = {
         },
         },
         postprocessing: {
         postprocessing: {
             occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
             occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } },
-            outline: { name: 'on', params: { scale: 1, threshold: 0.1 } }
+            outline: { name: 'on', params: { scale: 1, threshold: 0.1, color: Color(0x000000) } }
         },
         },
         renderer: {
         renderer: {
             style: { name: 'flat', params: {} }
             style: { name: 'flat', params: {} }
@@ -105,7 +106,7 @@ class LightingDemo {
                 ...this.plugin.canvas3d!.props.postprocessing,
                 ...this.plugin.canvas3d!.props.postprocessing,
                 ...props.postprocessing
                 ...props.postprocessing
             },
             },
-        }});
+        } });
     }
     }
 
 
     async load({ url, format = 'mmcif', isBinary = true, assemblyId = '' }: LoadParams, radius: number, bias: number) {
     async load({ url, format = 'mmcif', isBinary = true, assemblyId = '' }: LoadParams, radius: number, bias: number) {

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

@@ -250,7 +250,7 @@ class MolStarProteopediaWrapper {
     setBackground(color: number) {
     setBackground(color: number) {
         if (!this.plugin.canvas3d) return;
         if (!this.plugin.canvas3d) return;
         const renderer = this.plugin.canvas3d.props.renderer;
         const renderer = this.plugin.canvas3d.props.renderer;
-        PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer,  backgroundColor: Color(color) } } });
+        PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: Color(color) } } });
     }
     }
 
 
     toggleSpin() {
     toggleSpin() {

+ 38 - 19
src/extensions/anvil/algorithm.ts

@@ -39,22 +39,34 @@ interface ANVILContext {
 };
 };
 
 
 export const ANVILParams = {
 export const ANVILParams = {
-    numberOfSpherePoints: PD.Numeric(140, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
+    numberOfSpherePoints: PD.Numeric(175, { min: 35, max: 700, step: 1 }, { description: 'Number of spheres/directions to test for membrane placement. Original value is 350.' }),
     stepSize: PD.Numeric(1, { min: 0.25, max: 4, step: 0.25 }, { description: 'Thickness of membrane slices that will be tested' }),
     stepSize: PD.Numeric(1, { min: 0.25, max: 4, step: 0.25 }, { description: 'Thickness of membrane slices that will be tested' }),
-    minThickness: PD.Numeric(20, { min: 10, max: 30, step: 1}, { description: 'Minimum membrane thickness used during refinement' }),
-    maxThickness: PD.Numeric(40, { min: 30, max: 50, step: 1}, { description: 'Maximum membrane thickness used during refinement' }),
+    minThickness: PD.Numeric(20, { min: 10, max: 30, step: 1 }, { description: 'Minimum membrane thickness used during refinement' }),
+    maxThickness: PD.Numeric(40, { min: 30, max: 50, step: 1 }, { description: 'Maximum membrane thickness used during refinement' }),
     asaCutoff: PD.Numeric(40, { min: 10, max: 100, step: 1 }, { description: 'Relative ASA cutoff above which residues will be considered' }),
     asaCutoff: PD.Numeric(40, { min: 10, max: 100, step: 1 }, { description: 'Relative ASA cutoff above which residues will be considered' }),
-    adjust: PD.Numeric(14, { min: 0, max: 30, step: 1 }, { description: 'Minimum length of membrane-spanning regions (original values: 14 for alpha-helices and 5 for beta sheets). Set to 0 to not optimize membrane thickness.' })
+    adjust: PD.Numeric(14, { min: 0, max: 30, step: 1 }, { description: 'Minimum length of membrane-spanning regions (original values: 14 for alpha-helices and 5 for beta sheets). Set to 0 to not optimize membrane thickness.' }),
+    tmdetDefinition: PD.Boolean(false, { description: `Use TMDET's classification of membrane-favoring amino acids. TMDET's classification shows better performance on porins and other beta-barrel structures.` })
 };
 };
 export type ANVILParams = typeof ANVILParams
 export type ANVILParams = typeof ANVILParams
 export type ANVILProps = PD.Values<ANVILParams>
 export type ANVILProps = PD.Values<ANVILParams>
 
 
+/** ANVIL-specific (not general) definition of membrane-favoring amino acids */
+const ANVIL_DEFINITION = new Set(['ALA', 'CYS', 'GLY', 'HIS', 'ILE', 'LEU', 'MET', 'PHE', 'SER', 'TRP', 'VAL']);
+/** TMDET-specific (not general) definition of membrane-favoring amino acids */
+const TMDET_DEFINITION = new Set(['LEU', 'ILE', 'VAL', 'PHE', 'MET', 'GLY', 'TRP', 'TYR']);
+
 /**
 /**
  * Implements:
  * Implements:
  * Membrane positioning for high- and low-resolution protein structures through a binary classification approach
  * Membrane positioning for high- and low-resolution protein structures through a binary classification approach
  * Guillaume Postic, Yassine Ghouzam, Vincent Guiraud, and Jean-Christophe Gelly
  * Guillaume Postic, Yassine Ghouzam, Vincent Guiraud, and Jean-Christophe Gelly
  * Protein Engineering, Design & Selection, 2015, 1–5
  * Protein Engineering, Design & Selection, 2015, 1–5
  * doi: 10.1093/protein/gzv063
  * doi: 10.1093/protein/gzv063
+ *
+ * ANVIL is derived from TMDET, the corresponding classification of hydrophobic amino acids is provided as optional parameter:
+ * Gabor E. Tusnady, Zsuzsanna Dosztanyi and Istvan Simon
+ * Transmembrane proteins in the Protein Data Bank: identification and classification
+ * Bioinformatics, 2004, 2964-2972
+ * doi: 10.1093/bioinformatics/bth340
  */
  */
 export function computeANVIL(structure: Structure, props: ANVILProps) {
 export function computeANVIL(structure: Structure, props: ANVILProps) {
     return Task.create('Compute Membrane Orientation', async runtime => {
     return Task.create('Compute Membrane Orientation', async runtime => {
@@ -87,6 +99,11 @@ async function initialize(structure: Structure, props: ANVILProps, accessibleSur
     const offsets = new Array<number>();
     const offsets = new Array<number>();
     const exposed = new Array<number>();
     const exposed = new Array<number>();
     const hydrophobic = new Array<boolean>();
     const hydrophobic = new Array<boolean>();
+    const definition = props.tmdetDefinition ? TMDET_DEFINITION : ANVIL_DEFINITION;
+
+    function isPartOfEntity(l: StructureElement.Location): boolean {
+        return !Unit.isAtomic(l.unit) ? notAtomic() : l.unit.model.atomicHierarchy.residues.label_seq_id.valueKind(l.unit.residueIndex[l.element]) === 0;
+    }
 
 
     const vec = v3zero();
     const vec = v3zero();
     for (let i = 0, il = structure.units.length; i < il; ++i) {
     for (let i = 0, il = structure.units.length; i < il; ++i) {
@@ -98,8 +115,8 @@ async function initialize(structure: Structure, props: ANVILProps, accessibleSur
             const eI = elements[j];
             const eI = elements[j];
             l.element = eI;
             l.element = eI;
 
 
-            // consider only amino acids
-            if (getElementMoleculeType(unit, eI) !== MoleculeType.Protein) {
+            // consider only amino acids in chains
+            if (getElementMoleculeType(unit, eI) !== MoleculeType.Protein || !isPartOfEntity(l)) {
                 continue;
                 continue;
             }
             }
 
 
@@ -121,7 +138,7 @@ async function initialize(structure: Structure, props: ANVILProps, accessibleSur
             offsets.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
             offsets.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
             if (AccessibleSurfaceArea.getValue(l, accessibleSurfaceArea) / MaxAsa[label_comp_id(l)] > asaCutoff) {
             if (AccessibleSurfaceArea.getValue(l, accessibleSurfaceArea) / MaxAsa[label_comp_id(l)] > asaCutoff) {
                 exposed.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
                 exposed.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
-                hydrophobic.push(isHydrophobic(label_comp_id(l)));
+                hydrophobic.push(isHydrophobic(definition, label_comp_id(l)));
             }
             }
         }
         }
     }
     }
@@ -166,7 +183,7 @@ export async function calculate(runtime: RuntimeContext, structure: Structure, p
     }
     }
 
 
     const normalVector = v3zero();
     const normalVector = v3zero();
-    const center =  v3zero();
+    const center = v3zero();
     v3sub(normalVector, membrane.planePoint1, membrane.planePoint2);
     v3sub(normalVector, membrane.planePoint1, membrane.planePoint2);
     v3normalize(normalVector, normalVector);
     v3normalize(normalVector, normalVector);
 
 
@@ -344,7 +361,7 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
     // collect all residues in membrane layer
     // collect all residues in membrane layer
     for (let k = 0, kl = offsets.length; k < kl; k++) {
     for (let k = 0, kl = offsets.length; k < kl; k++) {
         const unit = units[unitIndices[offsets[k]]];
         const unit = units[unitIndices[offsets[k]]];
-        if (!Unit.isAtomic(unit)) throw 'Property only available for atomic models.';
+        if (!Unit.isAtomic(unit)) notAtomic();
         const elementIndex = elementIndices[offsets[k]];
         const elementIndex = elementIndices[offsets[k]];
 
 
         authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
         authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
@@ -365,7 +382,7 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
 
 
     for (let k = 0, kl = offsets.length; k < kl; k++) {
     for (let k = 0, kl = offsets.length; k < kl; k++) {
         const unit = units[unitIndices[offsets[k]]];
         const unit = units[unitIndices[offsets[k]]];
-        if (!Unit.isAtomic(unit)) throw 'Property only available for atomic models.';
+        if (!Unit.isAtomic(unit)) notAtomic();
         const elementIndex = elementIndices[offsets[k]];
         const elementIndex = elementIndices[offsets[k]];
 
 
         authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
         authAsymId = unit.model.atomicHierarchy.chains.auth_asym_id.value(unit.chainIndex[elementIndex]);
@@ -387,7 +404,7 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
                 }
                 }
                 lastAuthSeqId = authSeqId;
                 lastAuthSeqId = authSeqId;
                 endOffset = k;
                 endOffset = k;
-            } else  {
+            } else {
                 lastAuthSeqId++;
                 lastAuthSeqId++;
                 endOffset++;
                 endOffset++;
             }
             }
@@ -428,6 +445,10 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
     return refinedSegments;
     return refinedSegments;
 }
 }
 
 
+function notAtomic(): never {
+    throw new Error('Property only available for atomic models.');
+}
+
 /** Filter for membrane residues and calculate the final extent of the membrane layer */
 /** Filter for membrane residues and calculate the final extent of the membrane layer */
 function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid: Vec3): number {
 function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid: Vec3): number {
     const { offsets, structure } = ctx;
     const { offsets, structure } = ctx;
@@ -455,11 +476,11 @@ function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid:
 }
 }
 
 
 function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
 function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
-    if(initialStats.hphob < 1) {
+    if (initialStats.hphob < 1) {
         initialStats.hphob = 0.1;
         initialStats.hphob = 0.1;
     }
     }
 
 
-    if(initialStats.hphil < 1) {
+    if (initialStats.hphil < 1) {
         initialStats.hphil += 1;
         initialStats.hphil += 1;
     }
     }
 
 
@@ -484,7 +505,7 @@ function generateSpherePoints(ctx: ANVILContext, numberOfSpherePoints: number):
     const { centroid, extent } = ctx;
     const { centroid, extent } = ctx;
     const points = [];
     const points = [];
     let oldPhi = 0, h, theta, phi;
     let oldPhi = 0, h, theta, phi;
-    for(let k = 1, kl = numberOfSpherePoints + 1; k < kl; k++) {
+    for (let k = 1, kl = numberOfSpherePoints + 1; k < kl; k++) {
         h = -1 + 2 * (k - 1) / (2 * numberOfSpherePoints - 1);
         h = -1 + 2 * (k - 1) / (2 * numberOfSpherePoints - 1);
         theta = Math.acos(h);
         theta = Math.acos(h);
         phi = (k === 1 || k === numberOfSpherePoints) ? 0 : (oldPhi + 3.6 / Math.sqrt(2 * numberOfSpherePoints * (1 - h * h))) % (2 * Math.PI);
         phi = (k === 1 || k === numberOfSpherePoints) ? 0 : (oldPhi + 3.6 / Math.sqrt(2 * numberOfSpherePoints * (1 - h * h))) % (2 * Math.PI);
@@ -566,11 +587,9 @@ namespace HphobHphil {
     }
     }
 }
 }
 
 
-/** ANVIL-specific (not general) definition of membrane-favoring amino acids */
-const HYDROPHOBIC_AMINO_ACIDS = new Set(['ALA', 'CYS', 'GLY', 'HIS', 'ILE', 'LEU', 'MET', 'PHE', 'SER', 'TRP', 'VAL']);
-/** Returns true if ANVIL considers this as amino acid that favors being embedded in a membrane */
-export function isHydrophobic(label_comp_id: string): boolean {
-    return HYDROPHOBIC_AMINO_ACIDS.has(label_comp_id);
+/** Returns true if the definition considers this as membrane-favoring amino acid */
+export function isHydrophobic(definition: Set<string>, label_comp_id: string): boolean {
+    return definition.has(label_comp_id);
 }
 }
 
 
 /** Accessible surface area used for normalization. ANVIL uses 'Total-Side REL' values from NACCESS, from: Hubbard, S. J., & Thornton, J. M. (1993). naccess. Computer Program, Department of Biochemistry and Molecular Biology, University College London, 2(1). */
 /** Accessible surface area used for normalization. ANVIL uses 'Total-Side REL' values from NACCESS, from: Hubbard, S. J., & Thornton, J. M. (1993). naccess. Computer Program, Department of Biochemistry and Molecular Biology, University College London, 2(1). */

+ 2 - 2
src/extensions/anvil/behavior.ts

@@ -52,7 +52,7 @@ export const ANVILMembraneOrientation = PluginBehavior.create<{ autoAttach: bool
         }
         }
 
 
         update(p: { autoAttach: boolean }) {
         update(p: { autoAttach: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             return updated;
             return updated;
@@ -150,7 +150,7 @@ export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
     params: () => StructureRepresentationPresetProvider.CommonParams,
     params: () => StructureRepresentationPresetProvider.CommonParams,
     async apply(ref, params, plugin) {
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
-        const structure  = structureCell?.obj?.data;
+        const structure = structureCell?.obj?.data;
         if (!structureCell || !structure) return {};
         if (!structureCell || !structure) return {};
 
 
         if (!MembraneOrientationProvider.get(structure).value) {
         if (!MembraneOrientationProvider.get(structure).value) {

+ 3 - 3
src/extensions/cellpack/color/generate.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020-2021 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>
  */
  */
@@ -33,7 +33,7 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
 
 
     if (ctx.structure && info) {
     if (ctx.structure && info) {
         const colors = distinctColors(info.packingsCount);
         const colors = distinctColors(info.packingsCount);
-        let hcl = Hcl.fromColor(Hcl(), colors[info.packingIndex]);
+        const hcl = Hcl.fromColor(Hcl(), colors[info.packingIndex]);
 
 
         const hue = [Math.max(0, hcl[0] - 35), Math.min(360, hcl[0] + 35)] as [number, number];
         const hue = [Math.max(0, hcl[0] - 35), Math.min(360, hcl[0] + 35)] as [number, number];
 
 
@@ -48,7 +48,7 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
                 hue, chroma: [30, 80], luminance: [15, 85],
                 hue, chroma: [30, 80], luminance: [15, 85],
                 clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
                 clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
             }
             }
-        }}, { minLabel: 'Min', maxLabel: 'Max' });
+        } }, { minLabel: 'Min', maxLabel: 'Max' });
         legend = palette.legend;
         legend = palette.legend;
         const modelColor = new Map<number, Color>();
         const modelColor = new Map<number, Color>();
         for (let i = 0, il = models.length; i < il; ++i) {
         for (let i = 0, il = models.length; i < il; ++i) {

+ 1 - 1
src/extensions/cellpack/color/provided.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 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>
  */
  */

+ 7 - 7
src/extensions/cellpack/curve.ts

@@ -1,7 +1,7 @@
 /**
 /**
- * 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 Ludovic Autin <autin@scripps.edu>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
@@ -49,7 +49,7 @@ function ResampleControlPoints(points: NumberArray, segmentLength: number) {
     // controlPoints.Insert(0, controlPoints[0] + (controlPoints[0] - controlPoints[1]) / 2.0f);
     // controlPoints.Insert(0, controlPoints[0] + (controlPoints[0] - controlPoints[1]) / 2.0f);
     // controlPoints.Add(controlPoints[nP - 1] + (controlPoints[nP - 1] - controlPoints[nP - 2]) / 2.0f);
     // controlPoints.Add(controlPoints[nP - 1] + (controlPoints[nP - 1] - controlPoints[nP - 2]) / 2.0f);
 
 
-    let resampledControlPoints: Vec3[] = [];
+    const resampledControlPoints: Vec3[] = [];
     // resampledControlPoints.Add(controlPoints[0]);
     // resampledControlPoints.Add(controlPoints[0]);
     // resampledControlPoints.Add(controlPoints[1]);
     // resampledControlPoints.Add(controlPoints[1]);
 
 
@@ -111,7 +111,7 @@ function GetSmoothNormals(points: Vec3[]) {
     let p1 = points[1];
     let p1 = points[1];
     let p2 = points[2];
     let p2 = points[2];
     const p21 = Vec3.sub(tmpV1, p2, p1);
     const p21 = Vec3.sub(tmpV1, p2, p1);
-    const p01 =  Vec3.sub(tmpV2, p0, p1);
+    const p01 = Vec3.sub(tmpV2, p0, p1);
     const p0121 = Vec3.cross(tmpV3, p01, p21);
     const p0121 = Vec3.cross(tmpV3, p01, p21);
     Vec3.normalize(prevV, p0121);
     Vec3.normalize(prevV, p0121);
     smoothNormals.push(Vec3.clone(prevV));
     smoothNormals.push(Vec3.clone(prevV));
@@ -179,7 +179,7 @@ function GetMiniFrame(points: Vec3[], normals: Vec3[]) {
         const v1t = Vec3.scale(mfTmpV5, v1, (2.0 / c1) * Vec3.dot(v1, frames[i].t));
         const v1t = Vec3.scale(mfTmpV5, v1, (2.0 / c1) * Vec3.dot(v1, frames[i].t));
         const tan_L_i = Vec3.sub(mfTmpV6, frames[i].t, v1t);
         const tan_L_i = Vec3.sub(mfTmpV6, frames[i].t, v1t);
         // # compute reflection vector of R_2
         // # compute reflection vector of R_2
-        const v2 =  Vec3.sub(mfTmpV7, t2, tan_L_i);
+        const v2 = Vec3.sub(mfTmpV7, t2, tan_L_i);
         const c2 = Vec3.dot(v2, v2);
         const c2 = Vec3.dot(v2, v2);
         // compute r_(i+1) = R_2 * r_i^L
         // compute r_(i+1) = R_2 * r_i^L
         const v2l = Vec3.scale(mfTmpV8, v1, (2.0 / c2) * Vec3.dot(v2, ref_L_i));
         const v2l = Vec3.scale(mfTmpV8, v1, (2.0 / c2) * Vec3.dot(v2, ref_L_i));
@@ -195,7 +195,7 @@ export function getMatFromResamplePoints(points: NumberArray, segmentLength: num
     let new_points: Vec3[] = [];
     let new_points: Vec3[] = [];
     if (resample) new_points = ResampleControlPoints(points, segmentLength);
     if (resample) new_points = ResampleControlPoints(points, segmentLength);
     else {
     else {
-        for (let idx = 0; idx < points.length / 3; ++idx){
+        for (let idx = 0; idx < points.length / 3; ++idx) {
             new_points.push(Vec3.fromArray(Vec3.zero(), points, idx * 3));
             new_points.push(Vec3.fromArray(Vec3.zero(), points, idx * 3));
         }
         }
     }
     }
@@ -211,7 +211,7 @@ export function getMatFromResamplePoints(points: NumberArray, segmentLength: num
         if (d >= segmentLength) {
         if (d >= segmentLength) {
             // use twist or random?
             // use twist or random?
             const quat = Quat.rotationTo(Quat.zero(), Vec3.create(0, 0, 1), frames[i].t); // Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),new_normal[i]);//Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),direction);new_normal
             const quat = Quat.rotationTo(Quat.zero(), Vec3.create(0, 0, 1), frames[i].t); // Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),new_normal[i]);//Quat.rotationTo(Quat.zero(), Vec3.create(0,0,1),direction);new_normal
-            const rq = Quat.setAxisAngle(Quat.zero(), frames[i].t, Math.random() * 3.60 ); // Quat.setAxisAngle(Quat.zero(),direction, Math.random()*3.60 );//Quat.identity();//
+            const rq = Quat.setAxisAngle(Quat.zero(), frames[i].t, Math.random() * 3.60); // Quat.setAxisAngle(Quat.zero(),direction, Math.random()*3.60 );//Quat.identity();//
             const m = Mat4.fromQuat(Mat4.zero(), Quat.multiply(Quat.zero(), rq, quat)); // Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),quat1,quat2));//Mat4.fromQuat(Mat4.zero(),quat);//Mat4.identity();//Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),rq,quat));
             const m = Mat4.fromQuat(Mat4.zero(), Quat.multiply(Quat.zero(), rq, quat)); // Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),quat1,quat2));//Mat4.fromQuat(Mat4.zero(),quat);//Mat4.identity();//Mat4.fromQuat(Mat4.zero(),Quat.multiply(Quat.zero(),rq,quat));
             // let pos:Vec3 = Vec3.add(Vec3.zero(),pti1,pti)
             // let pos:Vec3 = Vec3.add(Vec3.zero(),pti1,pti)
             // pos = Vec3.scale(pos,pos,1.0/2.0);
             // pos = Vec3.scale(pos,pos,1.0/2.0);

+ 38 - 4
src/extensions/cellpack/data.ts

@@ -13,16 +13,27 @@ export interface CellPack {
 
 
 export interface CellPacking {
 export interface CellPacking {
     name: string,
     name: string,
-    location: 'surface' | 'interior' | 'cytoplasme',
+    location: 'surface' | 'interior' | 'cytoplasme'
     ingredients: Packing['ingredients']
     ingredients: Packing['ingredients']
+    compartment?: CellCompartment
 }
 }
 
 
-//
+export interface CellCompartment {
+    filename?: string
+    geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
+    compartment_primitives?: CompartmentPrimitives
+}
 
 
 export interface Cell {
 export interface Cell {
     recipe: Recipe
     recipe: Recipe
+    options?: RecipeOptions
     cytoplasme?: Packing
     cytoplasme?: Packing
     compartments?: { [key: string]: Compartment }
     compartments?: { [key: string]: Compartment }
+    mapping_ids?: { [key: number]: [number, string] }
+}
+
+export interface RecipeOptions {
+    resultfile?: string
 }
 }
 
 
 export interface Recipe {
 export interface Recipe {
@@ -35,8 +46,29 @@ export interface Recipe {
 export interface Compartment {
 export interface Compartment {
     surface?: Packing
     surface?: Packing
     interior?: Packing
     interior?: Packing
+    geom?: unknown
+    geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
+    mb?: CompartmentPrimitives
+}
+
+// Primitives discribing a compartment
+export const enum CompartmentPrimitiveType {
+    MetaBall = 0,
+    Sphere = 1,
+    Cube = 2,
+    Cylinder = 3,
+    Cone = 4,
+    Plane = 5,
+    None = 6
 }
 }
 
 
+export interface CompartmentPrimitives{
+    positions?: number[];
+    radii?: number[];
+    types?: CompartmentPrimitiveType[];
+}
+
+
 export interface Packing {
 export interface Packing {
     ingredients: { [key: string]: Ingredient }
     ingredients: { [key: string]: Ingredient }
 }
 }
@@ -64,18 +96,20 @@ export interface Ingredient {
     [curveX: string]: unknown;
     [curveX: string]: unknown;
     /** the orientation in the membrane */
     /** the orientation in the membrane */
     principalAxis?: Vec3;
     principalAxis?: Vec3;
+    principalVector?: Vec3;
     /** offset along membrane */
     /** offset along membrane */
     offset?: Vec3;
     offset?: Vec3;
     ingtype?: string;
     ingtype?: string;
     color?: Vec3;
     color?: Vec3;
     confidence?: number;
     confidence?: number;
+    Type?: string;
 }
 }
 
 
 export interface IngredientSource {
 export interface IngredientSource {
     pdb: string;
     pdb: string;
-    bu?: string;  /** biological unit e.g AU,BU1,etc.. */
+    bu?: string; /** biological unit e.g AU,BU1,etc.. */
     selection?: string; /** NGL selection or :A or :B etc.. */
     selection?: string; /** NGL selection or :A or :B etc.. */
-    model?: string;     /** model number e.g 0,1,2... */
+    model?: string; /** model number e.g 0,1,2... */
     transform: {
     transform: {
         center: boolean;
         center: boolean;
         translate?: Vec3;
         translate?: Vec3;

+ 1 - 1
src/extensions/cellpack/index.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 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>
  */
  */

+ 180 - 81
src/extensions/cellpack/model.ts

@@ -2,13 +2,14 @@
  * Copyright (c) 2019-2021 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>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
  */
 
 
 import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state';
 import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state';
 import { PluginContext } from '../../mol-plugin/context';
 import { PluginContext } from '../../mol-plugin/context';
 import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
 import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { Ingredient, IngredientSource, CellPacking } from './data';
+import { Ingredient, CellPacking, CompartmentPrimitives } from './data';
 import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
 import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
 import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
 import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
 import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
 import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
@@ -17,7 +18,7 @@ import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
 import { SymmetryOperator } from '../../mol-math/geometry';
 import { SymmetryOperator } from '../../mol-math/geometry';
 import { Task, RuntimeContext } from '../../mol-task';
 import { Task, RuntimeContext } from '../../mol-task';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
-import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies } from './state';
+import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies, CreateCompartmentSphere } from './state';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { getMatFromResamplePoints } from './curve';
 import { getMatFromResamplePoints } from './curve';
 import { compile } from '../../mol-script/runtime/query/compiler';
 import { compile } from '../../mol-script/runtime/query/compiler';
@@ -28,8 +29,9 @@ import { createModels } from '../../mol-model-formats/structure/basic/parser';
 import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
 import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
 import { Asset } from '../../mol-util/assets';
 import { Asset } from '../../mol-util/assets';
 import { Color } from '../../mol-util/color';
 import { Color } from '../../mol-util/color';
-import { readFromFile } from '../../mol-util/data-source';
 import { objectForEach } from '../../mol-util/object';
 import { objectForEach } from '../../mol-util/object';
+import { readFromFile } from '../../mol-util/data-source';
+import { ColorNames } from '../../mol-util/color/names';
 
 
 function getCellPackModelUrl(fileName: string, baseUrl: string) {
 function getCellPackModelUrl(fileName: string, baseUrl: string) {
     return `${baseUrl}/results/${fileName}`;
     return `${baseUrl}/results/${fileName}`;
@@ -41,12 +43,16 @@ class TrajectoryCache {
     get(id: string) { return this.map.get(id); }
     get(id: string) { return this.map.get(id); }
 }
 }
 
 
-async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient, baseUrl: string, trajCache: TrajectoryCache, file?: Asset.File) {
+async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient,
+    baseUrl: string, trajCache: TrajectoryCache, location: string,
+    file?: Asset.File
+) {
     const assetManager = plugin.managers.asset;
     const assetManager = plugin.managers.asset;
     const modelIndex = (ingredient.source.model) ? parseInt(ingredient.source.model) : 0;
     const modelIndex = (ingredient.source.model) ? parseInt(ingredient.source.model) : 0;
-    const surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false;
+    let surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false;
+    if (location === 'surface') surface = true;
     let trajectory = trajCache.get(id);
     let trajectory = trajCache.get(id);
-    let assets: Asset.Wrapper[] = [];
+    const assets: Asset.Wrapper[] = [];
     if (!trajectory) {
     if (!trajectory) {
         if (file) {
         if (file) {
             if (file.name.endsWith('.cif')) {
             if (file.name.endsWith('.cif')) {
@@ -68,10 +74,11 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
                 throw new Error(`unsupported file type '${file.name}'`);
                 throw new Error(`unsupported file type '${file.name}'`);
             }
             }
         } else if (id.match(/^[1-9][a-zA-Z0-9]{3,3}$/i)) {
         } else if (id.match(/^[1-9][a-zA-Z0-9]{3,3}$/i)) {
-            if (surface){
+            if (surface) {
                 try {
                 try {
                     const data = await getFromOPM(plugin, id, assetManager);
                     const data = await getFromOPM(plugin, id, assetManager);
                     assets.push(data.asset);
                     assets.push(data.asset);
+                    data.pdb.id! = id.toUpperCase();
                     trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
                     trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
                 } catch (e) {
                 } catch (e) {
                     // fallback to getFromPdb
                     // fallback to getFromPdb
@@ -100,7 +107,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
     return { model, assets };
     return { model, assets };
 }
 }
 
 
-async function getStructure(plugin: PluginContext, model: Model, source: IngredientSource, props: { assembly?: string } = {}) {
+async function getStructure(plugin: PluginContext, model: Model, source: Ingredient, props: { assembly?: string } = {}) {
     let structure = Structure.ofModel(model);
     let structure = Structure.ofModel(model);
     const { assembly } = props;
     const { assembly } = props;
 
 
@@ -108,11 +115,12 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
         structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly));
         structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly));
     }
     }
     let query;
     let query;
-    if (source.selection){
-        const asymIds: string[] = source.selection.replace(' ', '').replace(':', '').split('or');
+    if (source.source.selection) {
+        const sel = source.source.selection;
+        // selection can have the model ID as well. remove it
+        const asymIds: string[] = sel.replace(/ /g, '').replace(/:/g, '').split('or').slice(1);
         query = MS.struct.modifier.union([
         query = MS.struct.modifier.union([
             MS.struct.generator.atomGroups({
             MS.struct.generator.atomGroups({
-                'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
                 'chain-test': MS.core.set.has([MS.set(...asymIds), MS.ammp('auth_asym_id')])
                 'chain-test': MS.core.set.has([MS.set(...asymIds), MS.ammp('auth_asym_id')])
             })
             })
         ]);
         ]);
@@ -123,11 +131,11 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
             })
             })
         ]);
         ]);
     }
     }
-
     const compiled = compile<StructureSelection>(query);
     const compiled = compile<StructureSelection>(query);
     const result = compiled(new QueryContext(structure));
     const result = compiled(new QueryContext(structure));
     structure = StructureSelection.unionStructure(result);
     structure = StructureSelection.unionStructure(result);
-
+    // change here if possible the label ?
+    // structure.label =  source.name;
     return structure;
     return structure;
 }
 }
 
 
@@ -141,9 +149,9 @@ function getTransformLegacy(trans: Vec3, rot: Quat) {
 }
 }
 
 
 function getTransform(trans: Vec3, rot: Quat) {
 function getTransform(trans: Vec3, rot: Quat) {
-    const q: Quat = Quat.create(rot[0], rot[1], rot[2], rot[3]);
+    const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]);
     const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
     const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
-    const p: Vec3 = Vec3.create(trans[0], trans[1], trans[2]);
+    const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]);
     Mat4.setTranslation(m, p);
     Mat4.setTranslation(m, p);
     return m;
     return m;
 }
 }
@@ -157,9 +165,9 @@ function getCurveTransforms(ingredient: Ingredient) {
     const n = ingredient.nbCurve || 0;
     const n = ingredient.nbCurve || 0;
     const instances: Mat4[] = [];
     const instances: Mat4[] = [];
     let segmentLength = 3.4;
     let segmentLength = 3.4;
-    if (ingredient.uLength){
+    if (ingredient.uLength) {
         segmentLength = ingredient.uLength;
         segmentLength = ingredient.uLength;
-    } else if (ingredient.radii){
+    } else if (ingredient.radii) {
         segmentLength = ingredient.radii[0].radii
         segmentLength = ingredient.radii[0].radii
             ? ingredient.radii[0].radii[0] * 2.0
             ? ingredient.radii[0].radii[0] * 2.0
             : 3.4;
             : 3.4;
@@ -168,7 +176,7 @@ function getCurveTransforms(ingredient: Ingredient) {
     for (let i = 0; i < n; ++i) {
     for (let i = 0; i < n; ++i) {
         const cname = `curve${i}`;
         const cname = `curve${i}`;
         if (!(cname in ingredient)) {
         if (!(cname in ingredient)) {
-            // console.warn(`Expected '${cname}' in ingredient`)
+            console.warn(`Expected '${cname}' in ingredient`);
             continue;
             continue;
         }
         }
         const _points = ingredient[cname] as Vec3[];
         const _points = ingredient[cname] as Vec3[];
@@ -177,9 +185,9 @@ function getCurveTransforms(ingredient: Ingredient) {
             continue;
             continue;
         }
         }
         // test for resampling
         // test for resampling
-        let distance: number = Vec3.distance(_points[0], _points[1]);
+        const distance: number = Vec3.distance(_points[0], _points[1]);
         if (distance >= segmentLength + 2.0) {
         if (distance >= segmentLength + 2.0) {
-            console.info(distance);
+            // console.info(distance);
             resampling = true;
             resampling = true;
         }
         }
         const points = new Float32Array(_points.length * 3);
         const points = new Float32Array(_points.length * 3);
@@ -190,13 +198,13 @@ function getCurveTransforms(ingredient: Ingredient) {
     return instances;
     return instances;
 }
 }
 
 
-function getAssembly(transforms: Mat4[], structure: Structure) {
-    const builder = Structure.Builder();
+function getAssembly(name: string, transforms: Mat4[], structure: Structure) {
+    const builder = Structure.Builder({ label: name });
     const { units } = structure;
     const { units } = structure;
 
 
     for (let i = 0, il = transforms.length; i < il; ++i) {
     for (let i = 0, il = transforms.length; i < il; ++i) {
         const id = `${i + 1}`;
         const id = `${i + 1}`;
-        const op = SymmetryOperator.create(id, transforms[i], { assembly: { id, operId: i, operList: [ id ] } });
+        const op = SymmetryOperator.create(id, transforms[i], { assembly: { id, operId: i, operList: [id] } });
         for (const unit of units) {
         for (const unit of units) {
             builder.addWithOperator(unit, op);
             builder.addWithOperator(unit, op);
         }
         }
@@ -307,13 +315,13 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
     });
     });
 
 
     const curveModel = await plugin.runTask(curveModelTask);
     const curveModel = await plugin.runTask(curveModelTask);
-    return getStructure(plugin, curveModel, ingredient.source);
+    // ingredient.source.selection = undefined;
+    return getStructure(plugin, curveModel, ingredient);
 }
 }
 
 
-async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache) {
+async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') {
     const { name, source, results, nbCurve } = ingredient;
     const { name, source, results, nbCurve } = ingredient;
     if (source.pdb === 'None') return;
     if (source.pdb === 'None') return;
-
     const file = ingredientFiles[source.pdb];
     const file = ingredientFiles[source.pdb];
     if (!file) {
     if (!file) {
         // TODO can these be added to the library?
         // TODO can these be added to the library?
@@ -325,72 +333,79 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
     }
     }
 
 
     // model id in case structure is NMR
     // model id in case structure is NMR
-    const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, file);
+    const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, location, file);
     if (!model) return;
     if (!model) return;
-
     let structure: Structure;
     let structure: Structure;
     if (nbCurve) {
     if (nbCurve) {
         structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
         structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
     } else {
     } else {
+        if ((!results || results.length === 0)) return;
         let bu: string|undefined = source.bu ? source.bu : undefined;
         let bu: string|undefined = source.bu ? source.bu : undefined;
-        if (bu){
+        if (bu) {
             if (bu === 'AU') {
             if (bu === 'AU') {
                 bu = undefined;
                 bu = undefined;
             } else {
             } else {
                 bu = bu.slice(2);
                 bu = bu.slice(2);
             }
             }
         }
         }
-        structure = await getStructure(plugin, model, source, { assembly: bu });
+        structure = await getStructure(plugin, model, ingredient, { assembly: bu });
         // transform with offset and pcp
         // transform with offset and pcp
         let legacy: boolean = true;
         let legacy: boolean = true;
-        if (ingredient.offset || ingredient.principalAxis){
+        const pcp = ingredient.principalVector ? ingredient.principalVector : ingredient.principalAxis;
+        if (pcp) {
             legacy = false;
             legacy = false;
             const structureMean = getStructureMean(structure);
             const structureMean = getStructureMean(structure);
             Vec3.negate(structureMean, structureMean);
             Vec3.negate(structureMean, structureMean);
             const m1: Mat4 = Mat4.identity();
             const m1: Mat4 = Mat4.identity();
             Mat4.setTranslation(m1, structureMean);
             Mat4.setTranslation(m1, structureMean);
             structure = Structure.transform(structure, m1);
             structure = Structure.transform(structure, m1);
-            if (ingredient.offset){
-                if (!Vec3.exactEquals(ingredient.offset, Vec3.zero())){
+            if (ingredient.offset) {
+                const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]);
+                if (!Vec3.exactEquals(o, Vec3.zero())) { // -1, 1, 4e-16 ??
+                    if (location !== 'surface') {
+                        Vec3.negate(o, o);
+                    }
                     const m: Mat4 = Mat4.identity();
                     const m: Mat4 = Mat4.identity();
-                    Mat4.setTranslation(m, ingredient.offset);
+                    Mat4.setTranslation(m, o);
                     structure = Structure.transform(structure, m);
                     structure = Structure.transform(structure, m);
                 }
                 }
             }
             }
-            if (ingredient.principalAxis){
-                if (!Vec3.exactEquals(ingredient.principalAxis, Vec3.unitZ)){
+            if (pcp) {
+                const p: Vec3 = Vec3.create(pcp[0], pcp[1], pcp[2]);
+                if (!Vec3.exactEquals(p, Vec3.unitZ)) {
                     const q: Quat = Quat.identity();
                     const q: Quat = Quat.identity();
-                    Quat.rotationTo(q, ingredient.principalAxis, Vec3.unitZ);
+                    Quat.rotationTo(q, p, Vec3.unitZ);
                     const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
                     const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
                     structure = Structure.transform(structure, m);
                     structure = Structure.transform(structure, m);
                 }
                 }
             }
             }
         }
         }
-        structure = getAssembly(getResultTransforms(results, legacy), structure);
+
+        structure = getAssembly(name, getResultTransforms(results, legacy), structure);
     }
     }
 
 
     return { structure, assets };
     return { structure, assets };
 }
 }
 
 
+
 export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) {
 export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) {
     return Task.create('Create Packing Structure', async ctx => {
     return Task.create('Create Packing Structure', async ctx => {
-        const { ingredients, name } = packing;
+        const { ingredients, location, name } = packing;
         const assets: Asset.Wrapper[] = [];
         const assets: Asset.Wrapper[] = [];
         const trajCache = new TrajectoryCache();
         const trajCache = new TrajectoryCache();
         const structures: Structure[] = [];
         const structures: Structure[] = [];
         const colors: Color[] = [];
         const colors: Color[] = [];
-        let skipColors: boolean = false;
         for (const iName in ingredients) {
         for (const iName in ingredients) {
             if (ctx.shouldUpdate) await ctx.update(iName);
             if (ctx.shouldUpdate) await ctx.update(iName);
-            const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache);
+            const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache, location);
             if (ingredientStructure) {
             if (ingredientStructure) {
                 structures.push(ingredientStructure.structure);
                 structures.push(ingredientStructure.structure);
                 assets.push(...ingredientStructure.assets);
                 assets.push(...ingredientStructure.assets);
                 const c = ingredients[iName].color;
                 const c = ingredients[iName].color;
-                if (c){
+                if (c) {
                     colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
                     colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
                 } else {
                 } else {
-                    skipColors = true;
+                    colors.push(Color.fromNormalizedRgb(1, 0, 0));
                 }
                 }
             }
             }
         }
         }
@@ -402,7 +417,7 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
         for (const s of structures) {
         for (const s of structures) {
             if (ctx.shouldUpdate) await ctx.update(`${s.label}`);
             if (ctx.shouldUpdate) await ctx.update(`${s.label}`);
             let maxInvariantId = 0;
             let maxInvariantId = 0;
-            let maxChainGroupId = 0;
+            const maxChainGroupId = 0;
             for (const u of s.units) {
             for (const u of s.units) {
                 const invariantId = u.invariantId + offsetInvariantId;
                 const invariantId = u.invariantId + offsetInvariantId;
                 const chainGroupId = u.chainGroupId + offsetChainGroupId;
                 const chainGroupId = u.chainGroupId + offsetChainGroupId;
@@ -414,21 +429,20 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
         }
         }
 
 
         if (ctx.shouldUpdate) await ctx.update(`${name} - structure`);
         if (ctx.shouldUpdate) await ctx.update(`${name} - structure`);
-        const structure = Structure.create(units);
-        for( let i = 0, il = structure.models.length; i < il; ++i) {
+        const structure = Structure.create(units, { label: name + '.' + location });
+        for (let i = 0, il = structure.models.length; i < il; ++i) {
             Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
             Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
         }
         }
-        return { structure, assets, colors: skipColors ? undefined : colors };
+        return { structure, assets, colors: colors };
     });
     });
 }
 }
 
 
 async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) {
 async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) {
     for (let i = 0, il = packings.length; i < il; ++i) {
     for (let i = 0, il = packings.length; i < il; ++i) {
-        if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0') {
+        if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0' || packings[i].name === 'HIV_capsid') {
             const url = Asset.getUrlAsset(plugin.managers.asset, `${baseUrl}/extras/rna_allpoints.json`);
             const url = Asset.getUrlAsset(plugin.managers.asset, `${baseUrl}/extras/rna_allpoints.json`);
             const json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false));
             const json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false));
             const points = json.data.points as number[];
             const points = json.data.points as number[];
-
             const curve0: Vec3[] = [];
             const curve0: Vec3[] = [];
             for (let j = 0, jl = points.length; j < jl; j += 3) {
             for (let j = 0, jl = points.length; j < jl; j += 3) {
                 curve0.push(Vec3.fromArray(Vec3(), points, j));
                 curve0.push(Vec3.fromArray(Vec3(), points, j));
@@ -454,7 +468,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
                 break;
                 break;
             }
             }
         }
         }
-        if (!file){
+        if (!file) {
             // check for cif directly
             // check for cif directly
             const cifileName = `${name}.cif`;
             const cifileName = `${name}.cif`;
             for (const f of params.ingredients) {
             for (const f of params.ingredients) {
@@ -465,7 +479,8 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
             }
             }
         }
         }
     }
     }
-
+    let legacy_membrane: boolean = false; // temporary variable until all membrane are converted to the new correct cif format
+    let geometry_membrane: boolean = false; // membrane can be a mesh geometry
     let b = state.build().toRoot();
     let b = state.build().toRoot();
     if (file) {
     if (file) {
         if (file.name.endsWith('.cif')) {
         if (file.name.endsWith('.cif')) {
@@ -474,27 +489,82 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
             b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
             b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
         }
         }
     } else {
     } else {
-        const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
-        b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
+        if (name.toLowerCase().endsWith('.bcif')) {
+            const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
+            b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
+        } else if (name.toLowerCase().endsWith('.cif')) {
+            const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
+            b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
+        } else if (name.toLowerCase().endsWith('.ply')) {
+            const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/geometries/${name}`);
+            b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
+            geometry_membrane = true;
+        } else {
+            const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
+            b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
+            legacy_membrane = true;
+        }
     }
     }
-
-    const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
-        .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
-        .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
-        .apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
-        .commit({ revertOnError: true });
-
-    const membraneParams = {
-        representation: params.preset.representation,
+    const props = {
+        type: {
+            name: 'assembly' as const,
+            params: { id: '1' }
+        }
     };
     };
+    if (legacy_membrane) {
+        // old membrane
+        const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
+            .apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
+            .commit({ revertOnError: true });
+        const membraneParams = {
+            representation: params.preset.representation,
+        };
+        await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
+    } else if (geometry_membrane) {
+        await b.apply(StateTransforms.Data.ParsePly, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.ShapeFromPly)
+            .apply(StateTransforms.Representation.ShapeRepresentation3D, { xrayShaded: true,
+                doubleSided: true, coloring: { name: 'uniform', params: { color: ColorNames.orange } } })
+            .commit({ revertOnError: true });
+    } else {
+        const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
+            .apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } })
+            .commit({ revertOnError: true });
+        const membraneParams = {
+            representation: params.preset.representation,
+        };
+        await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
+    }
+}
 
 
-    await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
+async function handleMembraneSpheres(state: State, primitives: CompartmentPrimitives) {
+    const nSpheres = primitives.positions!.length / 3;
+    // console.log('ok mb ', nSpheres);
+    // TODO : take in account the type of the primitives.
+    for (let j = 0; j < nSpheres; j++) {
+        await state.build()
+            .toRoot()
+            .apply(CreateCompartmentSphere, {
+                center: Vec3.create(
+                    primitives.positions![j * 3 + 0],
+                    primitives.positions![j * 3 + 1],
+                    primitives.positions![j * 3 + 2]
+                ),
+                radius: primitives!.radii![j]
+            })
+            .commit();
+    }
 }
 }
 
 
 async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
 async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
     const ingredientFiles = params.ingredients || [];
     const ingredientFiles = params.ingredients || [];
 
 
     let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
     let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
+    let resultsFile: Asset.File | null = params.results;
     if (params.source.name === 'id') {
     if (params.source.name === 'id') {
         const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl));
         const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl));
         cellPackJson = state.build().toRoot()
         cellPackJson = state.build().toRoot()
@@ -506,29 +576,36 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
             return;
             return;
         }
         }
 
 
-        let jsonFile: Asset.File;
+        let modelFile: Asset.File;
         if (file.name.toLowerCase().endsWith('.zip')) {
         if (file.name.toLowerCase().endsWith('.zip')) {
             const data = await readFromFile(file.file, 'zip').runInContext(runtime);
             const data = await readFromFile(file.file, 'zip').runInContext(runtime);
-            jsonFile = Asset.File(new File([data['model.json']], 'model.json'));
+            if (data['model.json']) {
+                modelFile = Asset.File(new File([data['model.json']], 'model.json'));
+            } else {
+                throw new Error('model.json missing from zip file');
+            }
+            if (data['results.bin']) {
+                resultsFile = Asset.File(new File([data['results.bin']], 'results.bin'));
+            }
             objectForEach(data, (v, k) => {
             objectForEach(data, (v, k) => {
                 if (k === 'model.json') return;
                 if (k === 'model.json') return;
+                if (k === 'results.bin') return;
                 ingredientFiles.push(Asset.File(new File([v], k)));
                 ingredientFiles.push(Asset.File(new File([v], k)));
             });
             });
         } else {
         } else {
-            jsonFile = file;
+            modelFile = file;
         }
         }
-
         cellPackJson = state.build().toRoot()
         cellPackJson = state.build().toRoot()
-            .apply(StateTransforms.Data.ReadFile, { file: jsonFile, isBinary: false, label: jsonFile.name }, { state: { isGhost: true } });
+            .apply(StateTransforms.Data.ReadFile, { file: modelFile, isBinary: false, label: modelFile.name }, { state: { isGhost: true } });
     }
     }
 
 
     const cellPackBuilder = cellPackJson
     const cellPackBuilder = cellPackJson
         .apply(StateTransforms.Data.ParseJson, undefined, { state: { isGhost: true } })
         .apply(StateTransforms.Data.ParseJson, undefined, { state: { isGhost: true } })
-        .apply(ParseCellPack);
+        .apply(ParseCellPack, { resultsFile, baseUrl: params.baseUrl });
 
 
     const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime);
     const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime);
-    const { packings } = cellPackObject.obj!.data;
 
 
+    const { packings } = cellPackObject.obj!.data;
     await handleHivRna(plugin, packings, params.baseUrl);
     await handleHivRna(plugin, packings, params.baseUrl);
 
 
     for (let i = 0, il = packings.length; i < il; ++i) {
     for (let i = 0, il = packings.length; i < il; ++i) {
@@ -544,8 +621,30 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
             representation: params.preset.representation,
             representation: params.preset.representation,
         };
         };
         await CellpackPackingPreset.apply(packing, packingParams, plugin);
         await CellpackPackingPreset.apply(packing, packingParams, plugin);
-        if ( packings[i].location === 'surface' && params.membrane){
-            await loadMembrane(plugin, packings[i].name, state, params);
+        if (packings[i].compartment) {
+            if (params.membrane === 'lipids') {
+                if (packings[i].compartment!.geom_type) {
+                    if (packings[i].compartment!.geom_type === 'file') {
+                        // TODO: load mesh files or vertex,faces data
+                        await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
+                    } else if (packings[i].compartment!.compartment_primitives) {
+                        await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
+                    }
+                } else {
+                    // try loading membrane from repo as a bcif file or from the given list of files.
+                    if (params.membrane === 'lipids') {
+                        await loadMembrane(plugin, packings[i].name, state, params);
+                    }
+                }
+            } else if (params.membrane === 'geometry') {
+                if (packings[i].compartment!.compartment_primitives) {
+                    await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
+                } else if (packings[i].compartment!.geom_type === 'file') {
+                    if (packings[i].compartment!.filename!.toLowerCase().endsWith('.ply')) {
+                        await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
+                    }
+                }
+            }
         }
         }
     }
     }
 }
 }
@@ -555,19 +654,19 @@ const LoadCellPackModelParams = {
         'id': PD.Select('InfluenzaModel2.json', [
         'id': PD.Select('InfluenzaModel2.json', [
             ['blood_hiv_immature_inside.json', 'Blood HIV immature'],
             ['blood_hiv_immature_inside.json', 'Blood HIV immature'],
             ['HIV_immature_model.json', 'HIV immature'],
             ['HIV_immature_model.json', 'HIV immature'],
-            ['BloodHIV1.0_mixed_fixed_nc1.cpr', 'Blood HIV'],
-            ['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV'],
+            ['Blood_HIV.json', 'Blood HIV'],
+            ['HIV-1_0.1.6-8_mixed_radii_pdb.json', 'HIV'],
             ['influenza_model1.json', 'Influenza envelope'],
             ['influenza_model1.json', 'Influenza envelope'],
-            ['InfluenzaModel2.json', 'Influenza Complete'],
+            ['InfluenzaModel2.json', 'Influenza complete'],
             ['ExosomeModel.json', 'Exosome Model'],
             ['ExosomeModel.json', 'Exosome Model'],
-            ['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma simple'],
-            ['MycoplasmaModel.json', 'Mycoplasma WholeCell model'],
+            ['MycoplasmaGenitalium.json', 'Mycoplasma Genitalium curated model'],
         ] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }),
         ] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }),
-        'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.' }),
+        'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.', label: 'Recipe file' }),
     }, { options: [['id', 'Id'], ['file', 'File']] }),
     }, { options: [['id', 'Id'], ['file', 'File']] }),
     baseUrl: PD.Text(DefaultCellPackBaseUrl),
     baseUrl: PD.Text(DefaultCellPackBaseUrl),
-    membrane: PD.Boolean(true),
-    ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredients' }),
+    results: PD.File({ accept: '.bin', description: 'open results file in binary format from cellpackgpu for the specified recipe', label: 'Results file' }),
+    membrane: PD.Select('lipids', PD.arrayToOptions(['lipids', 'geometry', 'none'])),
+    ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }),
     preset: PD.Group({
     preset: PD.Group({
         traceOnly: PD.Boolean(false),
         traceOnly: PD.Boolean(false),
         representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
         representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
@@ -581,4 +680,4 @@ export const LoadCellPackModel = StateAction.build({
     from: PSO.Root
     from: PSO.Root
 })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
 })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
     await loadPackings(ctx, taskCtx, state, params);
     await loadPackings(ctx, taskCtx, state, params);
-}));
+}));

+ 5 - 6
src/extensions/cellpack/preset.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 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>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
  */
 
 
 import { StateObjectRef } from '../../mol-state';
 import { StateObjectRef } from '../../mol-state';
@@ -9,8 +10,6 @@ import { StructureRepresentationPresetProvider, presetStaticComponent } from '..
 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';
 import { CellPackGenerateColorThemeProvider } from './color/generate';
 import { CellPackGenerateColorThemeProvider } from './color/generate';
-import { CellPackInfoProvider } from './property';
-import { CellPackProvidedColorThemeProvider } from './color/provided';
 
 
 export const CellpackPackingPresetParams = {
 export const CellpackPackingPresetParams = {
     traceOnly: PD.Boolean(true),
     traceOnly: PD.Boolean(true),
@@ -42,8 +41,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
             Object.assign(reprProps, { sizeFactor: 2 });
             Object.assign(reprProps, { sizeFactor: 2 });
         }
         }
 
 
-        const info = structureCell.obj?.data && CellPackInfoProvider.get(structureCell.obj?.data).value;
-        const color = info?.colors ? CellPackProvidedColorThemeProvider.name : CellPackGenerateColorThemeProvider.name;
+        // default is generated
+        const color = CellPackGenerateColorThemeProvider.name;
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
         const representations = {
         const representations = {
@@ -92,4 +91,4 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
 
 
         return { components, representations };
         return { components, representations };
     }
     }
-});
+});

+ 2 - 2
src/extensions/cellpack/property.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 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>
  */
  */
@@ -34,4 +34,4 @@ export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellP
             value: { ...CellPackInfoParams.info.defaultValue, ...props.info }
             value: { ...CellPackInfoParams.info.defaultValue, ...props.info }
         };
         };
     }
     }
-});
+});

+ 70 - 0
src/extensions/cellpack/representation.ts

@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
+ */
+
+import { ShapeRepresentation } from '../../mol-repr/shape/representation';
+import { Shape } from '../../mol-model/shape';
+import { ColorNames } from '../../mol-util/color/names';
+import { RuntimeContext } from '../../mol-task';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
+// import { Polyhedron, DefaultPolyhedronProps } from '../../mol-geo/primitive/polyhedron';
+// import { Icosahedron } from '../../mol-geo/primitive/icosahedron';
+import { Sphere } from '../../mol-geo/primitive/sphere';
+import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
+import { RepresentationParamsGetter, Representation, RepresentationContext } from '../../mol-repr/representation';
+
+
+interface MembraneSphereData {
+    radius: number
+    center: Vec3
+}
+
+
+const MembraneSphereParams = {
+    ...Mesh.Params,
+    cellColor: PD.Color(ColorNames.orange),
+    cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
+    radius: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
+    center: PD.Vec3(Vec3.create(0, 0, 0)),
+    quality: { ...Mesh.Params.quality, isEssential: false },
+};
+
+type MeshParams = typeof MembraneSphereParams
+
+const MembraneSphereVisuals = {
+    'mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MeshParams>) => ShapeRepresentation(getMBShape, Mesh.Utils),
+};
+
+export const MBParams = {
+    ...MembraneSphereParams
+};
+export type MBParams = typeof MBParams
+export type UnitcellProps = PD.Values<MBParams>
+
+function getMBMesh(data: MembraneSphereData, props: UnitcellProps, mesh?: Mesh) {
+    const state = MeshBuilder.createState(256, 128, mesh);
+    const radius = props.radius;
+    const asphere = Sphere(3);
+    const trans: Mat4 = Mat4.identity();
+    Mat4.fromScaling(trans, Vec3.create(radius, radius, radius));
+    state.currentGroup = 1;
+    MeshBuilder.addPrimitive(state, trans, asphere);
+    const m = MeshBuilder.getMesh(state);
+    return m;
+}
+
+function getMBShape(ctx: RuntimeContext, data: MembraneSphereData, props: UnitcellProps, shape?: Shape<Mesh>) {
+    const geo = getMBMesh(data, props, shape && shape.geometry);
+    const label = 'mb';
+    return Shape.create(label, data, geo, () => props.cellColor, () => 1, () => label);
+}
+
+export type MBRepresentation = Representation<MembraneSphereData, MBParams>
+export function MBRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MBParams>): MBRepresentation {
+    return Representation.createMulti('MB', ctx, getParams, Representation.StateBuilder, MembraneSphereVisuals as unknown as Representation.Def<MembraneSphereData, MBParams>);
+}

+ 193 - 17
src/extensions/cellpack/state.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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
  */
 
 
 import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
 import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
@@ -15,9 +16,13 @@ import { PluginContext } from '../../mol-plugin/context';
 import { CellPackInfoProvider } from './property';
 import { CellPackInfoProvider } from './property';
 import { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure';
 import { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
+import { Vec3, Quat } from '../../mol-math/linear-algebra';
+import { StateTransformer } from '../../mol-state';
+import { MBRepresentation, MBParams } from './representation';
+import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
+import { getFloatValue } from './util';
 
 
-export const DefaultCellPackBaseUrl = 'https://mesoscope.scripps.edu/data/cellPACK_data/cellPACK_database_1.1.0/';
-
+export const DefaultCellPackBaseUrl = 'https://raw.githubusercontent.com/mesoscope/cellPACK_data/master/cellPACK_database_1.1.0';
 export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { }
 export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { }
 
 
 export { ParseCellPack };
 export { ParseCellPack };
@@ -26,26 +31,173 @@ const ParseCellPack = PluginStateTransform.BuiltIn({
     name: 'parse-cellpack',
     name: 'parse-cellpack',
     display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' },
     display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' },
     from: PSO.Format.Json,
     from: PSO.Format.Json,
-    to: CellPack
+    to: CellPack,
+    params: a => {
+        return {
+            resultsFile: PD.File({ accept: '.bin' }),
+            baseUrl: PD.Text(DefaultCellPackBaseUrl)
+        };
+    }
 })({
 })({
-    apply({ a }) {
+    apply({ a, params, cache }, plugin: PluginContext) {
         return Task.create('Parse CellPack', async ctx => {
         return Task.create('Parse CellPack', async ctx => {
             const cell = a.data as Cell;
             const cell = a.data as Cell;
-
+            let counter_id = 0;
+            let fiber_counter_id = 0;
+            let comp_counter = 0;
             const packings: CellPacking[] = [];
             const packings: CellPacking[] = [];
             const { compartments, cytoplasme } = cell;
             const { compartments, cytoplasme } = cell;
+            if (!cell.mapping_ids) cell.mapping_ids = {};
+            if (cytoplasme) {
+                packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients });
+                for (const iName in cytoplasme.ingredients) {
+                    if (cytoplasme.ingredients[iName].ingtype === 'fiber') {
+                        cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
+                        if (!cytoplasme.ingredients[iName].nbCurve) cytoplasme.ingredients[iName].nbCurve = 0;
+                        fiber_counter_id++;
+                    } else {
+                        cell.mapping_ids[counter_id] = [comp_counter, iName];
+                        if (!cytoplasme.ingredients[iName].results) { cytoplasme.ingredients[iName].results = []; }
+                        counter_id++;
+                    }
+                }
+                comp_counter++;
+            }
             if (compartments) {
             if (compartments) {
                 for (const name in compartments) {
                 for (const name in compartments) {
                     const { surface, interior } = compartments[name];
                     const { surface, interior } = compartments[name];
-                    if (surface) packings.push({ name, location: 'surface', ingredients: surface.ingredients });
-                    if (interior) packings.push({ name, location: 'interior', ingredients: interior.ingredients });
+                    let filename = '';
+                    if (compartments[name].geom_type === 'file') {
+                        filename = (compartments[name].geom) ? compartments[name].geom as string : '';
+                    }
+                    const compartment = { filename: filename, geom_type: compartments[name].geom_type, compartment_primitives: compartments[name].mb };
+                    if (surface) {
+                        packings.push({ name, location: 'surface', ingredients: surface.ingredients, compartment: compartment });
+                        for (const iName in surface.ingredients) {
+                            if (surface.ingredients[iName].ingtype === 'fiber') {
+                                cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
+                                if (!surface.ingredients[iName].nbCurve) surface.ingredients[iName].nbCurve = 0;
+                                fiber_counter_id++;
+                            } else {
+                                cell.mapping_ids[counter_id] = [comp_counter, iName];
+                                if (!surface.ingredients[iName].results) { surface.ingredients[iName].results = []; }
+                                counter_id++;
+                            }
+                        }
+                        comp_counter++;
+                    }
+                    if (interior) {
+                        if (!surface) packings.push({ name, location: 'interior', ingredients: interior.ingredients, compartment: compartment });
+                        else packings.push({ name, location: 'interior', ingredients: interior.ingredients });
+                        for (const iName in interior.ingredients) {
+                            if (interior.ingredients[iName].ingtype === 'fiber') {
+                                cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
+                                if (!interior.ingredients[iName].nbCurve) interior.ingredients[iName].nbCurve = 0;
+                                fiber_counter_id++;
+                            } else {
+                                cell.mapping_ids[counter_id] = [comp_counter, iName];
+                                if (!interior.ingredients[iName].results) { interior.ingredients[iName].results = []; }
+                                counter_id++;
+                            }
+                        }
+                        comp_counter++;
+                    }
                 }
                 }
             }
             }
-            if (cytoplasme) packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients });
+            const { options } = cell;
+            let resultsAsset: Asset.Wrapper<'binary'> | undefined;
+            if (params.resultsFile) {
+                resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(params.resultsFile, 'binary', true));
+            } else if (options?.resultfile) {
+                const url = `${params.baseUrl}/results/${options.resultfile}`;
+                resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(Asset.getUrlAsset(plugin.managers.asset, url), 'binary', true));
+            }
+            if (resultsAsset) {
+                (cache as any).asset = resultsAsset;
+                const results = resultsAsset.data;
+                // flip the byte order if needed
+                const buffer = IsNativeEndianLittle ? results.buffer : flipByteOrder(results, 4);
+                const numbers = new DataView(buffer);
+                const ninst = getFloatValue(numbers, 0);
+                const npoints = getFloatValue(numbers, 4);
+                const ncurve = getFloatValue(numbers, 8);
+
+                let offset = 12;
+
+                if (ninst !== 0) {
+                    const pos = new Float32Array(buffer, offset, ninst * 4);
+                    offset += ninst * 4 * 4;
+                    const quat = new Float32Array(buffer, offset, ninst * 4);
+                    offset += ninst * 4 * 4;
+
+                    for (let i = 0; i < ninst; i++) {
+                        const x: number = pos[i * 4 + 0];
+                        const y: number = pos[i * 4 + 1];
+                        const z: number = pos[i * 4 + 2];
+                        const ingr_id = pos[i * 4 + 3] as number;
+                        const pid = cell.mapping_ids![ingr_id];
+                        if (!packings[pid[0]].ingredients[pid[1]].results) {
+                            packings[pid[0]].ingredients[pid[1]].results = [];
+                        }
+                        packings[pid[0]].ingredients[pid[1]].results.push([Vec3.create(x, y, z),
+                            Quat.create(quat[i * 4 + 0], quat[i * 4 + 1], quat[i * 4 + 2], quat[i * 4 + 3])]);
+                    }
+                }
 
 
+                if (npoints !== 0) {
+                    const ctr_pos = new Float32Array(buffer, offset, npoints * 4);
+                    offset += npoints * 4 * 4;
+                    offset += npoints * 4 * 4;
+                    const ctr_info = new Float32Array(buffer, offset, npoints * 4);
+                    offset += npoints * 4 * 4;
+                    const curve_ids = new Float32Array(buffer, offset, ncurve * 4);
+                    offset += ncurve * 4 * 4;
+
+                    let counter = 0;
+                    let ctr_points: Vec3[] = [];
+                    let prev_ctype = 0;
+                    let prev_cid = 0;
+
+                    for (let i = 0; i < npoints; i++) {
+                        const x: number = -ctr_pos[i * 4 + 0];
+                        const y: number = ctr_pos[i * 4 + 1];
+                        const z: number = ctr_pos[i * 4 + 2];
+                        const cid: number = ctr_info[i * 4 + 0]; // curve id
+                        const ctype: number = curve_ids[cid * 4 + 0]; // curve type
+                        // cid  148 165 -1 0
+                        // console.log("cid ",cid,ctype,prev_cid,prev_ctype);//165,148
+                        if (prev_ctype !== ctype) {
+                            const pid = cell.mapping_ids![-prev_ctype - 1];
+                            const cname = `curve${counter}`;
+                            packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
+                            packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
+                            ctr_points = [];
+                            counter = 0;
+                        } else if (prev_cid !== cid) {
+                            ctr_points = [];
+                            const pid = cell.mapping_ids![-prev_ctype - 1];
+                            const cname = `curve${counter}`;
+                            packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
+                            counter += 1;
+                        }
+                        ctr_points.push(Vec3.create(x, y, z));
+                        prev_ctype = ctype;
+                        prev_cid = cid;
+                    }
+
+                    // do the last one
+                    const pid = cell.mapping_ids![-prev_ctype - 1];
+                    const cname = `curve${counter}`;
+                    packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
+                    packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
+                }
+            }
             return new CellPack({ cell, packings });
             return new CellPack({ cell, packings });
         });
         });
-    }
+    },
+    dispose({ cache }) {
+        ((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose();
+    },
 });
 });
 
 
 export { StructureFromCellpack };
 export { StructureFromCellpack };
@@ -77,14 +229,13 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
             await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
             await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
                 info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
                 info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
             });
             });
-
             (cache as any).assets = assets;
             (cache as any).assets = assets;
-            return new PSO.Molecule.Structure(structure, { label: packing.name });
+            return new PSO.Molecule.Structure(structure, { label: packing.name + '.' + packing.location });
         });
         });
     },
     },
     dispose({ b, cache }) {
     dispose({ b, cache }) {
         const assets = (cache as any).assets as Asset.Wrapper[];
         const assets = (cache as any).assets as Asset.Wrapper[];
-        if(assets) {
+        if (assets) {
             for (const a of assets) a.dispose();
             for (const a of assets) a.dispose();
         }
         }
 
 
@@ -115,17 +266,17 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
             // TODO: optimze
             // TODO: optimze
             // TODO: think of ways how to fast-track changes to this for animations
             // TODO: think of ways how to fast-track changes to this for animations
             const model = a.data;
             const model = a.data;
-            let initial_structure = Structure.ofModel(model);
+            const initial_structure = Structure.ofModel(model);
             const structures: Structure[] = [];
             const structures: Structure[] = [];
             let structure: Structure = initial_structure;
             let structure: Structure = initial_structure;
             // the list of asambly *?
             // the list of asambly *?
             const symmetry = ModelSymmetry.Provider.get(model);
             const symmetry = ModelSymmetry.Provider.get(model);
-            if (symmetry && symmetry.assemblies.length !== 0){
+            if (symmetry && symmetry.assemblies.length !== 0) {
                 for (const a of symmetry.assemblies) {
                 for (const a of symmetry.assemblies) {
                     const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
                     const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
                     structures.push(s);
                     structures.push(s);
                 }
                 }
-                const builder = Structure.Builder();
+                const builder = Structure.Builder({ label: 'Membrane' });
                 let offsetInvariantId = 0;
                 let offsetInvariantId = 0;
                 for (const s of structures) {
                 for (const s of structures) {
                     let maxInvariantId = 0;
                     let maxInvariantId = 0;
@@ -137,7 +288,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
                     offsetInvariantId += maxInvariantId + 1;
                     offsetInvariantId += maxInvariantId + 1;
                 }
                 }
                 structure = builder.getStructure();
                 structure = builder.getStructure();
-                for( let i = 0, il = structure.models.length; i < il; ++i) {
+                for (let i = 0, il = structure.models.length; i < il; ++i) {
                     Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
                     Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
                 }
                 }
             }
             }
@@ -148,3 +299,28 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
         b?.data.customPropertyDescriptors.dispose();
         b?.data.customPropertyDescriptors.dispose();
     }
     }
 });
 });
+
+const CreateTransformer = StateTransformer.builderFactory('cellPACK');
+export const CreateCompartmentSphere = CreateTransformer({
+    name: 'create-compartment-sphere',
+    display: 'CompartmentSphere',
+    from: PSO.Root, // or whatever data source
+    to: PSO.Shape.Representation3D,
+    params: {
+        center: PD.Vec3(Vec3()),
+        radius: PD.Numeric(1),
+        label: PD.Text(`Compartment Sphere`)
+    }
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        return true;
+    },
+    apply({ a, params }, plugin: PluginContext) {
+        return Task.create('Compartment Sphere', async ctx => {
+            const data = params;
+            const repr = MBRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => (MBParams));
+            await repr.createOrUpdate({ ...params, quality: 'custom', xrayShaded: true, doubleSided: true }, data).runInContext(ctx);
+            return new PSO.Shape.Representation3D({ repr, sourceData: a }, { label: data.label });
+        });
+    }
+});

+ 35 - 3
src/extensions/cellpack/util.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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
  */
 
 
 import { CIF } from '../../mol-io/reader/cif';
 import { CIF } from '../../mol-io/reader/cif';
@@ -37,11 +38,11 @@ async function downloadPDB(plugin: PluginContext, url: string, id: string, asset
 }
 }
 
 
 export async function getFromPdb(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
 export async function getFromPdb(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
-    const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId.toUpperCase()}.bcif`, true, assetManager);
+    const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId}.bcif`, true, assetManager);
     return { mmcif: cif.blocks[0], asset };
     return { mmcif: cif.blocks[0], asset };
 }
 }
 
 
-export async function getFromOPM(plugin: PluginContext, pdbId: string, assetManager: AssetManager){
+export async function getFromOPM(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
     const asset = await plugin.runTask(assetManager.resolve(Asset.getUrlAsset(assetManager, `https://opm-assets.storage.googleapis.com/pdb/${pdbId.toLowerCase()}.pdb`), 'string'));
     const asset = await plugin.runTask(assetManager.resolve(Asset.getUrlAsset(assetManager, `https://opm-assets.storage.googleapis.com/pdb/${pdbId.toLowerCase()}.pdb`), 'string'));
     return { pdb: await parsePDBfile(plugin, asset.data, pdbId), asset };
     return { pdb: await parsePDBfile(plugin, asset.data, pdbId), asset };
 }
 }
@@ -74,4 +75,35 @@ export function getStructureMean(structure: Structure) {
     }
     }
     const { elementCount } = structure;
     const { elementCount } = structure;
     return Vec3.create(xSum / elementCount, ySum / elementCount, zSum / elementCount);
     return Vec3.create(xSum / elementCount, ySum / elementCount, zSum / elementCount);
+}
+
+export function getFloatValue(value: DataView, offset: number) {
+    // if the last byte is a negative value (MSB is 1), the final
+    // float should be too
+    const negative = value.getInt8(offset + 2) >>> 31;
+
+    // this is how the bytes are arranged in the byte array/DataView
+    // buffer
+    const [b0, b1, b2, exponent] = [
+        // get first three bytes as unsigned since we only care
+        // about the last 8 bits of 32-bit js number returned by
+        // getUint8().
+        // Should be the same as: getInt8(offset) & -1 >>> 24
+        value.getUint8(offset),
+        value.getUint8(offset + 1),
+        value.getUint8(offset + 2),
+
+        // get the last byte, which is the exponent, as a signed int
+        // since it's already correct
+        value.getInt8(offset + 3)
+    ];
+
+    let mantissa = b0 | (b1 << 8) | (b2 << 16);
+    if (negative) {
+        // need to set the most significant 8 bits to 1's since a js
+        // number is 32 bits but our mantissa is only 24.
+        mantissa |= 255 << 24;
+    }
+
+    return mantissa * Math.pow(10, exponent);
 }
 }

+ 2 - 2
src/extensions/dnatco/confal-pyramids/behavior.ts

@@ -41,10 +41,10 @@ export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider(
 
 
         let pyramidsRepr;
         let pyramidsRepr;
         if (representations)
         if (representations)
-            pyramidsRepr = builder.buildRepresentation(update, pyramids,  { type: ConfalPyramidsRepresentationProvider, typeParams, color: ConfalPyramidsColorThemeProvider }, { tag: 'confal-pyramdis' } );
+            pyramidsRepr = builder.buildRepresentation(update, pyramids, { type: ConfalPyramidsRepresentationProvider, typeParams, color: ConfalPyramidsColorThemeProvider }, { tag: 'confal-pyramdis' });
 
 
         await update.commit({ revertOnError: true });
         await update.commit({ revertOnError: true });
-        return  { components: { ...components, pyramids }, representations: { ...representations, pyramidsRepr } };
+        return { components: { ...components, pyramids }, representations: { ...representations, pyramidsRepr } };
     }
     }
 });
 });
 
 

+ 3 - 3
src/extensions/dnatco/confal-pyramids/color.ts

@@ -27,7 +27,7 @@ const ColorMapping: ReadonlyMap<ConformerClasses, Color> = new Map([
     ['B', Color(0xC8CFFF)],
     ['B', Color(0xC8CFFF)],
     ['BII', Color(0x0059DA)],
     ['BII', Color(0x0059DA)],
     ['miB', Color(0x3BE8FB)],
     ['miB', Color(0x3BE8FB)],
-    ['Z',  Color(0x01F60E)],
+    ['Z', Color(0x01F60E)],
     ['IC', Color(0xFA5CFB)],
     ['IC', Color(0xFA5CFB)],
     ['OPN', Color(0xE90000)],
     ['OPN', Color(0xE90000)],
     ['SYN', Color(0xFFFF01)],
     ['SYN', Color(0xFFFF01)],
@@ -165,8 +165,8 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
         legend: TableLegend(iterableToArray(ColorMapping.entries()).map(([conformer, color]) => {
         legend: TableLegend(iterableToArray(ColorMapping.entries()).map(([conformer, color]) => {
             return [conformer, color] as [string, Color];
             return [conformer, color] as [string, Color];
         }).concat([
         }).concat([
-            [ 'Error', ErrorColor ],
-            [ 'Unknown', DefaultColor ]
+            ['Error', ErrorColor],
+            ['Unknown', DefaultColor]
         ]))
         ]))
     };
     };
 }
 }

+ 1 - 1
src/extensions/dnatco/confal-pyramids/util.ts

@@ -95,7 +95,7 @@ export namespace ConfalPyramidsUtil {
             const first = residueInfoFromLocation(locFirst);
             const first = residueInfoFromLocation(locFirst);
             const second = residueInfoFromLocation(locSecond);
             const second = residueInfoFromLocation(locSecond);
             const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
             const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
-            const alt_id_1 =  fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
+            const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
             const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
             const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
             const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
             const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
             const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';
             const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';

+ 1 - 1
src/extensions/g3d/model.ts

@@ -61,7 +61,7 @@ function getColumns(block: G3dDataBlock) {
     objectForEach(data, (hs, h) => {
     objectForEach(data, (hs, h) => {
         objectForEach(hs, (chs, ch) => {
         objectForEach(hs, (chs, ch) => {
             const entity_id = `${ch}-${h}`;
             const entity_id = `${ch}-${h}`;
-            const l =  chs.start.length;
+            const l = chs.start.length;
             if (l === 0) return;
             if (l === 0) return;
 
 
             let x = chs.x[0];
             let x = chs.x[0];

+ 3 - 5
src/extensions/geo-export/controls.ts

@@ -4,7 +4,6 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
  */
 
 
-import { getStyle } from '../../mol-gl/renderer';
 import { Box3D } from '../../mol-math/geometry';
 import { Box3D } from '../../mol-math/geometry';
 import { PluginComponent } from '../../mol-plugin-state/component';
 import { PluginComponent } from '../../mol-plugin-state/component';
 import { PluginContext } from '../../mol-plugin/context';
 import { PluginContext } from '../../mol-plugin/context';
@@ -46,13 +45,12 @@ export class GeometryControls extends PluginComponent {
                 const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
                 const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
                 const filename = this.getFilename();
                 const filename = this.getFilename();
 
 
-                const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
                 const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
                 const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
                 const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
                 const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
                 let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
                 let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
                 switch (this.behaviors.params.value.format) {
                 switch (this.behaviors.params.value.format) {
                     case 'glb':
                     case 'glb':
-                        renderObjectExporter = new GlbExporter(style, boundingBox);
+                        renderObjectExporter = new GlbExporter(boundingBox);
                         break;
                         break;
                     case 'obj':
                     case 'obj':
                         renderObjectExporter = new ObjExporter(filename, boundingBox);
                         renderObjectExporter = new ObjExporter(filename, boundingBox);
@@ -61,7 +59,7 @@ export class GeometryControls extends PluginComponent {
                         renderObjectExporter = new StlExporter(boundingBox);
                         renderObjectExporter = new StlExporter(boundingBox);
                         break;
                         break;
                     case 'usdz':
                     case 'usdz':
-                        renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius);
+                        renderObjectExporter = new UsdzExporter(boundingBox, boundingSphere.radius);
                         break;
                         break;
                     default: throw new Error('Unsupported format.');
                     default: throw new Error('Unsupported format.');
                 }
                 }
@@ -77,7 +75,7 @@ export class GeometryControls extends PluginComponent {
                     filename: filename + '.' + renderObjectExporter.fileExtension
                     filename: filename + '.' + renderObjectExporter.fileExtension
                 };
                 };
             } catch (e) {
             } catch (e) {
-                this.plugin.log.error('' + e);
+                this.plugin.log.error('Error during geometry export');
                 throw e;
                 throw e;
             }
             }
         });
         });

+ 60 - 66
src/extensions/geo-export/glb-exporter.ts

@@ -2,10 +2,9 @@
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
-import { BaseValues } from '../../mol-gl/renderable/schema';
-import { Style } from '../../mol-gl/renderer';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
 import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
 import { Box3D } from '../../mol-math/geometry';
 import { Box3D } from '../../mol-math/geometry';
@@ -15,7 +14,7 @@ import { RuntimeContext } from '../../mol-task';
 import { Color } from '../../mol-util/color/color';
 import { Color } from '../../mol-util/color/color';
 import { fillSerial } from '../../mol-util/array';
 import { fillSerial } from '../../mol-util/array';
 import { NumberArray } from '../../mol-util/type-helpers';
 import { NumberArray } from '../../mol-util/type-helpers';
-import { MeshExporter, AddMeshInput } from './mesh-exporter';
+import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
 
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3fromArray = Vec3.fromArray;
 const v3fromArray = Vec3.fromArray;
@@ -30,6 +29,12 @@ const FLOAT = 5126;
 const ARRAY_BUFFER = 34962;
 const ARRAY_BUFFER = 34962;
 const ELEMENT_ARRAY_BUFFER = 34963;
 const ELEMENT_ARRAY_BUFFER = 34963;
 
 
+const GLTF_MAGIC_BYTE = 0x46546C67;
+const JSON_CHUNK_TYPE = 0x4E4F534A;
+const BIN_CHUNK_TYPE = 0x004E4942;
+const JSON_PAD_CHAR = 0x20;
+const BIN_PAD_CHAR = 0x00;
+
 export type GlbData = {
 export type GlbData = {
     glb: Uint8Array
     glb: Uint8Array
 }
 }
@@ -38,6 +43,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
     readonly fileExtension = 'glb';
     readonly fileExtension = 'glb';
     private nodes: Record<string, any>[] = [];
     private nodes: Record<string, any>[] = [];
     private meshes: Record<string, any>[] = [];
     private meshes: Record<string, any>[] = [];
+    private materials: Record<string, any>[] = [];
+    private materialMap = new Map<string, number>();
     private accessors: Record<string, any>[] = [];
     private accessors: Record<string, any>[] = [];
     private bufferViews: Record<string, any>[] = [];
     private bufferViews: Record<string, any>[] = [];
     private binaryBuffer: ArrayBuffer[] = [];
     private binaryBuffer: ArrayBuffer[] = [];
@@ -53,7 +60,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
                 max[j] = Math.max(a[i + j], max[j]);
                 max[j] = Math.max(a[i + j], max[j]);
             }
             }
         }
         }
-        return [ min, max ];
+        return [min, max];
     }
     }
 
 
     private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
     private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
@@ -108,7 +115,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
             indexArray = indices!.slice(0, drawCount);
             indexArray = indices!.slice(0, drawCount);
         }
         }
 
 
-        const [ vertexMin, vertexMax ] = GlbExporter.vec3MinMax(vertexArray);
+        const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
 
 
         let vertexBuffer = vertexArray.buffer;
         let vertexBuffer = vertexArray.buffer;
         let normalBuffer = normalArray.buffer;
         let normalBuffer = normalArray.buffer;
@@ -126,57 +133,17 @@ export class GlbExporter extends MeshExporter<GlbData> {
         };
         };
     }
     }
 
 
-    private addColorBuffer(values: BaseValues, groups: Float32Array | Uint8Array, vertexCount: number, instanceIndex: number, isGeoTexture: boolean, interpolatedColors: Uint8Array) {
-        const groupCount = values.uGroupCount.ref.value;
-        const colorType = values.dColorType.ref.value;
-        const uColor = values.uColor.ref.value;
-        const tColor = values.tColor.ref.value.array;
+    private addColorBuffer(geoData: MeshGeoData, interpolatedColors: Uint8Array | undefined, interpolatedOverpaint: Uint8Array | undefined, interpolatedTransparency: Uint8Array | undefined) {
+        const { values, vertexCount } = geoData;
         const uAlpha = values.uAlpha.ref.value;
         const uAlpha = values.uAlpha.ref.value;
-        const dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
 
 
         const colorArray = new Uint8Array(vertexCount * 4);
         const colorArray = new Uint8Array(vertexCount * 4);
 
 
         for (let i = 0; i < vertexCount; ++i) {
         for (let i = 0; i < vertexCount; ++i) {
-            let color: Color;
-            switch (colorType) {
-                case 'uniform':
-                    color = Color.fromNormalizedArray(uColor, 0);
-                    break;
-                case 'instance':
-                    color = Color.fromArray(tColor, instanceIndex * 3);
-                    break;
-                case 'group': {
-                    const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                    color = Color.fromArray(tColor, group * 3);
-                    break;
-                }
-                case 'groupInstance': {
-                    const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                    color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
-                    break;
-                }
-                case 'vertex':
-                    color = Color.fromArray(tColor, i * 3);
-                    break;
-                case 'vertexInstance':
-                    color = Color.fromArray(tColor, (instanceIndex * vertexCount + i) * 3);
-                    break;
-                case 'volume':
-                    color = Color.fromArray(interpolatedColors!, i * 3);
-                    break;
-                case 'volumeInstance':
-                    color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + i) * 3);
-                    break;
-                default: throw new Error('Unsupported color type.');
-            }
+            let color = GlbExporter.getColor(i, geoData, interpolatedColors, interpolatedOverpaint);
 
 
-            let alpha = uAlpha;
-            if (dTransparency) {
-                const group = isGeoTexture ? GlbExporter.getGroup(groups, i) : groups[i];
-                const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
-                alpha *= 1 - transparency;
-            }
+            const transparency = GlbExporter.getTransparency(i, geoData, interpolatedTransparency);
+            const alpha = uAlpha * (1 - transparency);
 
 
             color = Color.sRGBToLinear(color);
             color = Color.sRGBToLinear(color);
             Color.toArray(color, colorArray, i * 4);
             Color.toArray(color, colorArray, i * 4);
@@ -191,20 +158,53 @@ export class GlbExporter extends MeshExporter<GlbData> {
         return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
         return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
     }
     }
 
 
+    private addMaterial(metalness: number, roughness: number) {
+        const hash = `${metalness}|${roughness}`;
+        if (!this.materialMap.has(hash)) {
+            this.materialMap.set(hash, this.materials.length);
+            this.materials.push({
+                pbrMetallicRoughness: {
+                    baseColorFactor: [1, 1, 1, 1],
+                    metallicFactor: metalness,
+                    roughnessFactor: roughness
+                }
+            });
+        }
+        return this.materialMap.get(hash)!;
+    }
+
     protected async addMeshWithColors(input: AddMeshInput) {
     protected async addMeshWithColors(input: AddMeshInput) {
         const { mesh, values, isGeoTexture, webgl, ctx } = input;
         const { mesh, values, isGeoTexture, webgl, ctx } = input;
 
 
         const t = Mat4();
         const t = Mat4();
 
 
         const colorType = values.dColorType.ref.value;
         const colorType = values.dColorType.ref.value;
+        const overpaintType = values.dOverpaintType.ref.value;
+        const transparencyType = values.dTransparencyType.ref.value;
         const dTransparency = values.dTransparency.ref.value;
         const dTransparency = values.dTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
         const aTransform = values.aTransform.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
+        const metalness = values.uMetalness.ref.value;
+        const roughness = values.uRoughness.ref.value;
+
+        const material = this.addMaterial(metalness, roughness);
 
 
-        let interpolatedColors: Uint8Array;
+        let interpolatedColors: Uint8Array | undefined;
         if (colorType === 'volume' || colorType === 'volumeInstance') {
         if (colorType === 'volume' || colorType === 'volumeInstance') {
             const stride = isGeoTexture ? 4 : 3;
             const stride = isGeoTexture ? 4 : 3;
-            interpolatedColors = GlbExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
+            interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
+        }
+
+        let interpolatedOverpaint: Uint8Array | undefined;
+        if (overpaintType === 'volumeInstance') {
+            const stride = isGeoTexture ? 4 : 3;
+            interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
+        }
+
+        let interpolatedTransparency: Uint8Array | undefined;
+        if (transparencyType === 'volumeInstance') {
+            const stride = isGeoTexture ? 4 : 3;
+            interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
         }
         }
 
 
         // instancing
         // instancing
@@ -235,7 +235,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
 
 
                 // create a color buffer if needed
                 // create a color buffer if needed
                 if (instanceIndex === 0 || !sameColorBuffer) {
                 if (instanceIndex === 0 || !sameColorBuffer) {
-                    colorAccessorIndex = this.addColorBuffer(values, groups, vertexCount, instanceIndex, isGeoTexture, interpolatedColors!);
+                    colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
                 }
                 }
 
 
                 // glTF mesh
                 // glTF mesh
@@ -248,7 +248,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
                             COLOR_0: colorAccessorIndex!
                             COLOR_0: colorAccessorIndex!
                         },
                         },
                         indices: indexAccessorIndex,
                         indices: indexAccessorIndex,
-                        material: 0
+                        material
                     }]
                     }]
                 });
                 });
             }
             }
@@ -282,13 +282,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
             }],
             }],
             bufferViews: this.bufferViews,
             bufferViews: this.bufferViews,
             accessors: this.accessors,
             accessors: this.accessors,
-            materials: [{
-                pbrMetallicRoughness: {
-                    baseColorFactor: [1, 1, 1, 1],
-                    metallicFactor: this.style.metalness,
-                    roughnessFactor: this.style.roughness
-                }
-            }]
+            materials: this.materials
         };
         };
 
 
         const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
         const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
@@ -307,19 +301,19 @@ export class GlbExporter extends MeshExporter<GlbData> {
             if (padding) {
             if (padding) {
                 chunk.push(padding.buffer);
                 chunk.push(padding.buffer);
             }
             }
-            return [ chunk, 8 + byteLength ];
+            return [chunk, 8 + byteLength];
         };
         };
         const jsonString = JSON.stringify(gltf);
         const jsonString = JSON.stringify(gltf);
         const jsonBuffer = new Uint8Array(jsonString.length);
         const jsonBuffer = new Uint8Array(jsonString.length);
         asciiWrite(jsonBuffer, jsonString);
         asciiWrite(jsonBuffer, jsonString);
 
 
-        const [ jsonChunk, jsonChunkLength ] = createChunk(0x4E4F534A, [jsonBuffer.buffer], jsonBuffer.length, 0x20);
-        const [ binaryChunk, binaryChunkLength ] = createChunk(0x004E4942, this.binaryBuffer, binaryBufferLength, 0x00);
+        const [jsonChunk, jsonChunkLength] = createChunk(JSON_CHUNK_TYPE, [jsonBuffer.buffer], jsonBuffer.length, JSON_PAD_CHAR);
+        const [binaryChunk, binaryChunkLength] = createChunk(BIN_CHUNK_TYPE, this.binaryBuffer, binaryBufferLength, BIN_PAD_CHAR);
 
 
         const glbBufferLength = 12 + jsonChunkLength + binaryChunkLength;
         const glbBufferLength = 12 + jsonChunkLength + binaryChunkLength;
         const header = new ArrayBuffer(12);
         const header = new ArrayBuffer(12);
         const headerDataView = new DataView(header);
         const headerDataView = new DataView(header);
-        headerDataView.setUint32(0, 0x46546C67, true); // magic number "glTF"
+        headerDataView.setUint32(0, GLTF_MAGIC_BYTE, true); // magic number "glTF"
         headerDataView.setUint32(4, 2, true); // version
         headerDataView.setUint32(4, 2, true); // version
         headerDataView.setUint32(8, glbBufferLength, true); // length
         headerDataView.setUint32(8, glbBufferLength, true); // length
         const glbBuffer = [header, ...jsonChunk, ...binaryChunk];
         const glbBuffer = [header, ...jsonChunk, ...binaryChunk];
@@ -337,7 +331,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
         return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
         return new Blob([(await this.getData()).glb], { type: 'model/gltf-binary' });
     }
     }
 
 
-    constructor(private style: Style, boundingBox: Box3D) {
+    constructor(boundingBox: Box3D) {
         super();
         super();
         const tmpV = Vec3();
         const tmpV = Vec3();
         Vec3.add(tmpV, boundingBox.min, boundingBox.max);
         Vec3.add(tmpV, boundingBox.min, boundingBox.max);

+ 147 - 12
src/extensions/geo-export/mesh-exporter.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import { sort, arraySwap } from '../../mol-data/util';
 import { sort, arraySwap } from '../../mol-data/util';
@@ -26,6 +27,7 @@ import { RuntimeContext } from '../../mol-task';
 import { Color } from '../../mol-util/color/color';
 import { Color } from '../../mol-util/color/color';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
 import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
 import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
+import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
 
 
 const GeoExportName = 'geo-export';
 const GeoExportName = 'geo-export';
 
 
@@ -48,6 +50,14 @@ export interface AddMeshInput {
     ctx: RuntimeContext
     ctx: RuntimeContext
 }
 }
 
 
+export type MeshGeoData = {
+    values: BaseValues,
+    groups: Float32Array | Uint8Array,
+    vertexCount: number,
+    instanceIndex: number,
+    isGeoTexture: boolean
+}
+
 export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
 export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
     abstract readonly fileExtension: string;
     abstract readonly fileExtension: string;
 
 
@@ -90,26 +100,43 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         return decodeFloatRGB(r, g, b);
         return decodeFloatRGB(r, g, b);
     }
     }
 
 
-    protected static getInterpolatedColors(vertices: Float32Array, vertexCount: number, values: BaseValues, stride: number, colorType: 'volume' | 'volumeInstance', webgl: WebGLContext) {
+    protected static getInterpolatedColors(webgl: WebGLContext, input: { vertices: Float32Array, vertexCount: number, values: BaseValues, stride: 3 | 4, colorType: 'volume' | 'volumeInstance' }) {
+        const { values, vertexCount, vertices, colorType, stride } = input;
         const colorGridTransform = values.uColorGridTransform.ref.value;
         const colorGridTransform = values.uColorGridTransform.ref.value;
         const colorGridDim = values.uColorGridDim.ref.value;
         const colorGridDim = values.uColorGridDim.ref.value;
         const colorTexDim = values.uColorTexDim.ref.value;
         const colorTexDim = values.uColorTexDim.ref.value;
         const aTransform = values.aTransform.ref.value;
         const aTransform = values.aTransform.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
 
 
-        if (!webgl.namedFramebuffers[GeoExportName]) {
-            webgl.namedFramebuffers[GeoExportName] = webgl.resources.framebuffer();
-        }
-        const framebuffer = webgl.namedFramebuffers[GeoExportName];
+        const colorGrid = readTexture(webgl, values.tColorGrid.ref.value).array;
+        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: colorGrid, gridDim: colorGridDim, gridTexDim: colorTexDim, gridTransform: colorGridTransform, vertexStride: stride, colorStride: 4, outputStride: 3 });
+        return interpolated.array;
+    }
 
 
-        const [ width, height ] = colorTexDim;
-        const colorGrid = new Uint8Array(width * height * 4);
+    protected static getInterpolatedOverpaint(webgl: WebGLContext, input: { vertices: Float32Array, vertexCount: number, values: BaseValues, stride: 3 | 4, colorType: 'volumeInstance' }) {
+        const { values, vertexCount, vertices, colorType, stride } = input;
+        const overpaintGridTransform = values.uOverpaintGridTransform.ref.value;
+        const overpaintGridDim = values.uOverpaintGridDim.ref.value;
+        const overpaintTexDim = values.uOverpaintTexDim.ref.value;
+        const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
 
 
-        framebuffer.bind();
-        values.tColorGrid.ref.value.attachFramebuffer(framebuffer, 0);
-        webgl.readPixels(0, 0, width, height, colorGrid);
+        const overpaintGrid = readTexture(webgl, values.tOverpaintGrid.ref.value).array;
+        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: overpaintGrid, gridDim: overpaintGridDim, gridTexDim: overpaintTexDim, gridTransform: overpaintGridTransform, vertexStride: stride, colorStride: 4, outputStride: 4 });
+        return interpolated.array;
+    }
+
+    protected static getInterpolatedTransparency(webgl: WebGLContext, input: { vertices: Float32Array, vertexCount: number, values: BaseValues, stride: 3 | 4, colorType: 'volumeInstance' }) {
+        const { values, vertexCount, vertices, colorType, stride } = input;
+        const transparencyGridTransform = values.uTransparencyGridTransform.ref.value;
+        const transparencyGridDim = values.uTransparencyGridDim.ref.value;
+        const transparencyTexDim = values.uTransparencyTexDim.ref.value;
+        const aTransform = values.aTransform.ref.value;
+        const instanceCount = values.uInstanceCount.ref.value;
+
+        const transparencyGrid = readAlphaTexture(webgl, values.tTransparencyGrid.ref.value).array;
+        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: transparencyGrid, gridDim: transparencyGridDim, gridTexDim: transparencyTexDim, gridTransform: transparencyGridTransform, vertexStride: stride, colorStride: 4, outputStride: 1, itemOffset: 3 });
 
 
-        const interpolated = getTrilinearlyInterpolated({ vertexCount, instanceCount, transformBuffer: aTransform, positionBuffer: vertices, colorType, grid: colorGrid, gridDim: colorGridDim, gridTexDim: colorTexDim, gridTransform: colorGridTransform, vertexStride: stride, colorStride: 4 });
         return interpolated.array;
         return interpolated.array;
     }
     }
 
 
@@ -194,6 +221,114 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         }
         }
     }
     }
 
 
+    protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
+        const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
+        const groupCount = values.uGroupCount.ref.value;
+        const colorType = values.dColorType.ref.value;
+        const uColor = values.uColor.ref.value;
+        const tColor = values.tColor.ref.value.array;
+        const overpaintType = values.dOverpaintType.ref.value;
+        const dOverpaint = values.dOverpaint.ref.value;
+        const tOverpaint = values.tOverpaint.ref.value.array;
+
+        let color: Color;
+        switch (colorType) {
+            case 'uniform':
+                color = Color.fromNormalizedArray(uColor, 0);
+                break;
+            case 'instance':
+                color = Color.fromArray(tColor, instanceIndex * 3);
+                break;
+            case 'group': {
+                const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
+                color = Color.fromArray(tColor, group * 3);
+                break;
+            }
+            case 'groupInstance': {
+                const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
+                color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
+                break;
+            }
+            case 'vertex':
+                color = Color.fromArray(tColor, vertexIndex * 3);
+                break;
+            case 'vertexInstance':
+                color = Color.fromArray(tColor, (instanceIndex * vertexCount + vertexIndex) * 3);
+                break;
+            case 'volume':
+                color = Color.fromArray(interpolatedColors!, vertexIndex * 3);
+                break;
+            case 'volumeInstance':
+                color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + vertexIndex) * 3);
+                break;
+            default: throw new Error('Unsupported color type.');
+        }
+
+        if (dOverpaint) {
+            let overpaintColor: Color;
+            let overpaintAlpha: number;
+            switch (overpaintType) {
+                case 'groupInstance': {
+                    const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
+                    const idx = (instanceIndex * groupCount + group) * 4;
+                    overpaintColor = Color.fromArray(tOverpaint, idx);
+                    overpaintAlpha = tOverpaint[idx + 3] / 255;
+                    break;
+                }
+                case 'vertexInstance': {
+                    const idx = (instanceIndex * vertexCount + vertexIndex) * 4;
+                    overpaintColor = Color.fromArray(tOverpaint, idx);
+                    overpaintAlpha = tOverpaint[idx + 3] / 255;
+                    break;
+                }
+                case 'volumeInstance': {
+                    const idx = (instanceIndex * vertexCount + vertexIndex) * 4;
+                    overpaintColor = Color.fromArray(interpolatedOverpaint!, idx);
+                    overpaintAlpha = interpolatedOverpaint![idx + 3] / 255;
+                    break;
+                }
+                default: throw new Error('Unsupported overpaint type.');
+            }
+            // interpolate twice to avoid darkening due to empty overpaint
+            overpaintColor = Color.interpolate(color, overpaintColor, overpaintAlpha);
+            color = Color.interpolate(color, overpaintColor, overpaintAlpha);
+        }
+
+        return color;
+    }
+
+    protected static getTransparency(vertexIndex: number, geoData: MeshGeoData, interpolatedTransparency?: Uint8Array): number {
+        const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
+        const groupCount = values.uGroupCount.ref.value;
+        const dTransparency = values.dTransparency.ref.value;
+        const tTransparency = values.tTransparency.ref.value.array;
+        const transparencyType = values.dTransparencyType.ref.value;
+
+        let transparency: number = 0;
+        if (dTransparency) {
+            switch (transparencyType) {
+                case 'groupInstance': {
+                    const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
+                    const idx = (instanceIndex * groupCount + group);
+                    transparency = tTransparency[idx] / 255;
+                    break;
+                }
+                case 'vertexInstance': {
+                    const idx = (instanceIndex * vertexCount + vertexIndex);
+                    transparency = tTransparency[idx] / 255;
+                    break;
+                }
+                case 'volumeInstance': {
+                    const idx = (instanceIndex * vertexCount + vertexIndex);
+                    transparency = interpolatedTransparency![idx] / 255;
+                    break;
+                }
+                default: throw new Error('Unsupported transparency type.');
+            }
+        }
+        return transparency;
+    }
+
     protected abstract addMeshWithColors(input: AddMeshInput): void;
     protected abstract addMeshWithColors(input: AddMeshInput): void;
 
 
     private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
     private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
@@ -306,7 +441,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         }
         }
         const framebuffer = webgl.namedFramebuffers[GeoExportName];
         const framebuffer = webgl.namedFramebuffers[GeoExportName];
 
 
-        const [ width, height ] = values.uGeoTexDim.ref.value;
+        const [width, height] = values.uGeoTexDim.ref.value;
         const vertices = new Float32Array(width * height * 4);
         const vertices = new Float32Array(width * height * 4);
         const normals = new Float32Array(width * height * 4);
         const normals = new Float32Array(width * height * 4);
         const groups = webgl.isWebGL2 ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4);
         const groups = webgl.isWebGL2 ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4);

+ 32 - 47
src/extensions/geo-export/obj-exporter.ts

@@ -2,6 +2,7 @@
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { asciiWrite } from '../../mol-io/common/ascii';
@@ -47,7 +48,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
         StringBuilder.newline(this.obj);
         StringBuilder.newline(this.obj);
         if (!this.materialSet.has(material)) {
         if (!this.materialSet.has(material)) {
             this.materialSet.add(material);
             this.materialSet.add(material);
-            const [r, g, b] = Color.toRgbNormalized(color);
+            const [r, g, b] = Color.toRgbNormalized(color).map(v => Math.round(v * 1000) / 1000);
             const mtl = this.mtl;
             const mtl = this.mtl;
             StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
             StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
             StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
             StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
@@ -77,19 +78,27 @@ export class ObjExporter extends MeshExporter<ObjData> {
         const tmpV = Vec3();
         const tmpV = Vec3();
         const stride = isGeoTexture ? 4 : 3;
         const stride = isGeoTexture ? 4 : 3;
 
 
-        const groupCount = values.uGroupCount.ref.value;
         const colorType = values.dColorType.ref.value;
         const colorType = values.dColorType.ref.value;
-        const tColor = values.tColor.ref.value.array;
+        const overpaintType = values.dOverpaintType.ref.value;
+        const transparencyType = values.dTransparencyType.ref.value;
         const uAlpha = values.uAlpha.ref.value;
         const uAlpha = values.uAlpha.ref.value;
-        const dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
         const aTransform = values.aTransform.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
 
 
-        let interpolatedColors: Uint8Array;
+        let interpolatedColors: Uint8Array | undefined;
         if (colorType === 'volume' || colorType === 'volumeInstance') {
         if (colorType === 'volume' || colorType === 'volumeInstance') {
-            interpolatedColors = ObjExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
-            ObjExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
+            interpolatedColors = ObjExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
+        }
+
+        let interpolatedOverpaint: Uint8Array | undefined;
+        if (overpaintType === 'volumeInstance') {
+            interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
+        }
+
+        let interpolatedTransparency: Uint8Array | undefined;
+        if (transparencyType === 'volumeInstance') {
+            const stride = isGeoTexture ? 4 : 3;
+            interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
         }
         }
 
 
         await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
         await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -127,47 +136,23 @@ export class ObjExporter extends MeshExporter<ObjData> {
                 StringBuilder.newline(obj);
                 StringBuilder.newline(obj);
             }
             }
 
 
+            const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
+
+            // color
+            const quantizedColors = new Uint8Array(drawCount * 3);
+            for (let i = 0; i < drawCount; i += 3) {
+                const v = isGeoTexture ? i : indices![i];
+                const color = ObjExporter.getColor(v, geoData, interpolatedColors, interpolatedOverpaint);
+                Color.toArray(color, quantizedColors, i);
+            }
+            ObjExporter.quantizeColors(quantizedColors, vertexCount);
+
             // face
             // face
             for (let i = 0; i < drawCount; i += 3) {
             for (let i = 0; i < drawCount; i += 3) {
-                let color: Color;
-                switch (colorType) {
-                    case 'uniform':
-                        color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
-                        break;
-                    case 'instance':
-                        color = Color.fromArray(tColor, instanceIndex * 3);
-                        break;
-                    case 'group': {
-                        const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                        color = Color.fromArray(tColor, group * 3);
-                        break;
-                    }
-                    case 'groupInstance': {
-                        const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                        color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
-                        break;
-                    }
-                    case 'vertex':
-                        color = Color.fromArray(tColor, indices![i] * 3);
-                        break;
-                    case 'vertexInstance':
-                        color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
-                        break;
-                    case 'volume':
-                        color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
-                        break;
-                    case 'volumeInstance':
-                        color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
-                        break;
-                    default: throw new Error('Unsupported color type.');
-                }
-
-                let alpha = uAlpha;
-                if (dTransparency) {
-                    const group = isGeoTexture ? ObjExporter.getGroup(groups, i) : groups[indices![i]];
-                    const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
-                    alpha *= 1 - transparency;
-                }
+                const color = Color.fromArray(quantizedColors, i);
+
+                const transparency = ObjExporter.getTransparency(i, geoData, interpolatedTransparency);
+                const alpha = Math.round(uAlpha * (1 - transparency) * 10) / 10; // quantized
 
 
                 this.updateMaterial(color, alpha);
                 this.updateMaterial(color, alpha);
 
 

+ 6 - 5
src/extensions/geo-export/ui.tsx

@@ -79,10 +79,10 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         try {
         try {
             this.setState({ busy: true });
             this.setState({ busy: true });
             const data = await this.controls.exportGeometry();
             const data = await this.controls.exportGeometry();
-            this.setState({ busy: false });
-
             download(data.blob, data.filename);
             download(data.blob, data.filename);
-        } catch {
+        } catch (e) {
+            console.error(e);
+        } finally {
             this.setState({ busy: false });
             this.setState({ busy: false });
         }
         }
     }
     }
@@ -91,7 +91,6 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         try {
         try {
             this.setState({ busy: true });
             this.setState({ busy: true });
             const data = await this.controls.exportGeometry();
             const data = await this.controls.exportGeometry();
-            this.setState({ busy: false });
             const a = document.createElement('a');
             const a = document.createElement('a');
             a.rel = 'ar';
             a.rel = 'ar';
             a.href = URL.createObjectURL(data.blob);
             a.href = URL.createObjectURL(data.blob);
@@ -100,7 +99,9 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
             a.appendChild(document.createElement('img'));
             a.appendChild(document.createElement('img'));
             setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
             setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
             setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
             setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
-        } catch {
+        } catch (e) {
+            console.error(e);
+        } finally {
             this.setState({ busy: false });
             this.setState({ busy: false });
         }
         }
     }
     }

+ 46 - 61
src/extensions/geo-export/usdz-exporter.ts

@@ -2,9 +2,9 @@
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
-import { Style } from '../../mol-gl/renderer';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { asciiWrite } from '../../mol-io/common/ascii';
 import { Box3D } from '../../mol-math/geometry';
 import { Box3D } from '../../mol-math/geometry';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
@@ -31,18 +31,17 @@ export class UsdzExporter extends MeshExporter<UsdzData> {
     readonly fileExtension = 'usdz';
     readonly fileExtension = 'usdz';
     private meshes: string[] = [];
     private meshes: string[] = [];
     private materials: string[] = [];
     private materials: string[] = [];
-    private materialSet = new Set<number>();
+    private materialMap = new Map<string, number>();
     private centerTransform: Mat4;
     private centerTransform: Mat4;
 
 
-    private static getMaterialKey(color: Color, alpha: number) {
-        return color * 256 + Math.round(alpha * 255);
-    }
+    private addMaterial(color: Color, alpha: number, metalness: number, roughness: number): number {
+        const hash = `${color}|${alpha}|${metalness}|${roughness}`;
+        if (this.materialMap.has(hash)) return this.materialMap.get(hash)!;
+
+        const materialKey = this.materialMap.size;
+        this.materialMap.set(hash, materialKey);
 
 
-    private addMaterial(color: Color, alpha: number) {
-        const materialKey = UsdzExporter.getMaterialKey(color, alpha);
-        if (this.materialSet.has(materialKey)) return;
-        this.materialSet.add(materialKey);
-        const [r, g, b] = Color.toRgbNormalized(color);
+        const [r, g, b] = Color.toRgbNormalized(color).map(v => Math.round(v * 1000) / 1000);
         this.materials.push(`
         this.materials.push(`
 def Material "material${materialKey}"
 def Material "material${materialKey}"
 {
 {
@@ -52,12 +51,13 @@ def Material "material${materialKey}"
         uniform token info:id = "UsdPreviewSurface"
         uniform token info:id = "UsdPreviewSurface"
         color3f inputs:diffuseColor = (${r},${g},${b})
         color3f inputs:diffuseColor = (${r},${g},${b})
         float inputs:opacity = ${alpha}
         float inputs:opacity = ${alpha}
-        float inputs:metallic = ${this.style.metalness}
-        float inputs:roughness = ${this.style.roughness}
+        float inputs:metallic = ${metalness}
+        float inputs:roughness = ${roughness}
         token outputs:surface
         token outputs:surface
     }
     }
 }
 }
 `);
 `);
+        return materialKey;
     }
     }
 
 
     protected async addMeshWithColors(input: AddMeshInput) {
     protected async addMeshWithColors(input: AddMeshInput) {
@@ -68,19 +68,30 @@ def Material "material${materialKey}"
         const tmpV = Vec3();
         const tmpV = Vec3();
         const stride = isGeoTexture ? 4 : 3;
         const stride = isGeoTexture ? 4 : 3;
 
 
-        const groupCount = values.uGroupCount.ref.value;
         const colorType = values.dColorType.ref.value;
         const colorType = values.dColorType.ref.value;
-        const tColor = values.tColor.ref.value.array;
+        const overpaintType = values.dOverpaintType.ref.value;
+        const transparencyType = values.dTransparencyType.ref.value;
         const uAlpha = values.uAlpha.ref.value;
         const uAlpha = values.uAlpha.ref.value;
-        const dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
         const aTransform = values.aTransform.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
+        const metalness = values.uMetalness.ref.value;
+        const roughness = values.uRoughness.ref.value;
 
 
-        let interpolatedColors: Uint8Array;
+        let interpolatedColors: Uint8Array | undefined;
         if (colorType === 'volume' || colorType === 'volumeInstance') {
         if (colorType === 'volume' || colorType === 'volumeInstance') {
-            interpolatedColors = UsdzExporter.getInterpolatedColors(mesh!.vertices, mesh!.vertexCount, values, stride, colorType, webgl!);
-            UsdzExporter.quantizeColors(interpolatedColors, mesh!.vertexCount);
+            interpolatedColors = UsdzExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
+        }
+
+        let interpolatedOverpaint: Uint8Array | undefined;
+        if (overpaintType === 'volumeInstance') {
+            const stride = isGeoTexture ? 4 : 3;
+            interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
+        }
+
+        let interpolatedTransparency: Uint8Array | undefined;
+        if (transparencyType === 'volumeInstance') {
+            const stride = isGeoTexture ? 4 : 3;
+            interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
         }
         }
 
 
         await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
         await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
@@ -122,6 +133,8 @@ def Material "material${materialKey}"
                 StringBuilder.writeSafe(normalBuilder, ')');
                 StringBuilder.writeSafe(normalBuilder, ')');
             }
             }
 
 
+            const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
+
             // face
             // face
             for (let i = 0; i < drawCount; ++i) {
             for (let i = 0; i < drawCount; ++i) {
                 const v = isGeoTexture ? i : indices![i];
                 const v = isGeoTexture ? i : indices![i];
@@ -130,51 +143,23 @@ def Material "material${materialKey}"
             }
             }
 
 
             // color
             // color
-            const faceIndicesByMaterial = new Map<number, number[]>();
+            const quantizedColors = new Uint8Array(drawCount * 3);
             for (let i = 0; i < drawCount; i += 3) {
             for (let i = 0; i < drawCount; i += 3) {
-                let color: Color;
-                switch (colorType) {
-                    case 'uniform':
-                        color = Color.fromNormalizedArray(values.uColor.ref.value, 0);
-                        break;
-                    case 'instance':
-                        color = Color.fromArray(tColor, instanceIndex * 3);
-                        break;
-                    case 'group': {
-                        const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
-                        color = Color.fromArray(tColor, group * 3);
-                        break;
-                    }
-                    case 'groupInstance': {
-                        const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
-                        color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
-                        break;
-                    }
-                    case 'vertex':
-                        color = Color.fromArray(tColor, indices![i] * 3);
-                        break;
-                    case 'vertexInstance':
-                        color = Color.fromArray(tColor, (instanceIndex * vertexCount + indices![i]) * 3);
-                        break;
-                    case 'volume':
-                        color = Color.fromArray(interpolatedColors!, (isGeoTexture ? i : indices![i]) * 3);
-                        break;
-                    case 'volumeInstance':
-                        color = Color.fromArray(interpolatedColors!, (instanceIndex * vertexCount + (isGeoTexture ? i : indices![i])) * 3);
-                        break;
-                    default: throw new Error('Unsupported color type.');
-                }
+                const v = isGeoTexture ? i : indices![i];
+                const color = UsdzExporter.getColor(v, geoData, interpolatedColors, interpolatedOverpaint);
+                Color.toArray(color, quantizedColors, i);
+            }
+            UsdzExporter.quantizeColors(quantizedColors, vertexCount);
 
 
-                let alpha = uAlpha;
-                if (dTransparency) {
-                    const group = isGeoTexture ? UsdzExporter.getGroup(groups, i) : groups[indices![i]];
-                    const transparency = tTransparency.array[instanceIndex * groupCount + group] / 255;
-                    alpha *= 1 - transparency;
-                }
+            // material
+            const faceIndicesByMaterial = new Map<number, number[]>();
+            for (let i = 0; i < drawCount; i += 3) {
+                const color = Color.fromArray(quantizedColors, i);
 
 
-                this.addMaterial(color, alpha);
+                const transparency = UsdzExporter.getTransparency(i, geoData, interpolatedTransparency);
+                const alpha = Math.round(uAlpha * (1 - transparency) * 10) / 10; // quantized
 
 
-                const materialKey = UsdzExporter.getMaterialKey(color, alpha);
+                const materialKey = this.addMaterial(color, alpha, metalness, roughness);
                 let faceIndices = faceIndicesByMaterial.get(materialKey);
                 let faceIndices = faceIndicesByMaterial.get(materialKey);
                 if (faceIndices === undefined) {
                 if (faceIndices === undefined) {
                     faceIndices = [];
                     faceIndices = [];
@@ -246,7 +231,7 @@ def Mesh "mesh${this.meshes.length}"
         return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
         return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
     }
     }
 
 
-    constructor(private style: Style, boundingBox: Box3D, radius: number) {
+    constructor(boundingBox: Box3D, radius: number) {
         super();
         super();
         const t = Mat4();
         const t = Mat4();
         // scale the model so that it fits within 1 meter
         // scale the model so that it fits within 1 meter

+ 1 - 1
src/extensions/mp4-export/controls.ts

@@ -73,7 +73,7 @@ export class Mp4Controls extends PluginComponent {
                 const filename = anim.anim.display.name.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
                 const filename = anim.anim.display.name.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
                 return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
                 return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
             } catch (e) {
             } catch (e) {
-                this.plugin.log.error('' + e);
+                this.plugin.log.error('Error during animation export');
                 throw e;
                 throw e;
             }
             }
         });
         });

+ 2 - 1
src/extensions/mp4-export/ui.tsx

@@ -115,7 +115,8 @@ export class Mp4EncoderUI extends CollapsableControls<{}, State> {
             this.setState({ busy: true });
             this.setState({ busy: true });
             const data = await this.controls.render();
             const data = await this.controls.render();
             this.setState({ busy: false, data });
             this.setState({ busy: false, data });
-        } catch {
+        } catch (e) {
+            console.error(e);
             this.setState({ busy: false });
             this.setState({ busy: false });
         }
         }
     }
     }

+ 1 - 1
src/extensions/pdbe/preferred-assembly.ts

@@ -62,7 +62,7 @@ export namespace PDBePreferredAssembly {
         if (model.customProperties.has(Descriptor)) return true;
         if (model.customProperties.has(Descriptor)) return true;
 
 
         let asmName: string | undefined = fromCifData(model);
         let asmName: string | undefined = fromCifData(model);
-        if (asmName === void 0 &&  params.PDBe_apiSourceJson) {
+        if (asmName === void 0 && params.PDBe_apiSourceJson) {
             const data = await params.PDBe_apiSourceJson(model);
             const data = await params.PDBe_apiSourceJson(model);
             if (!data) return false;
             if (!data) return false;
             asmName = asmNameFromJson(model, data);
             asmName = asmNameFromJson(model, data);

+ 1 - 1
src/extensions/pdbe/structure-quality-report/behavior.ts

@@ -52,7 +52,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
         }
         }
 
 
         update(p: { autoAttach: boolean, showTooltip: boolean }) {
         update(p: { autoAttach: boolean, showTooltip: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.showTooltip = p.showTooltip;
             this.params.showTooltip = p.showTooltip;
             this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);

+ 1 - 1
src/extensions/pdbe/structure-quality-report/color.ts

@@ -87,7 +87,7 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
     };
     };
 }
 }
 
 
-export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params, 'pdbe-structure-quality-report'> =  {
+export const StructureQualityReportColorThemeProvider: ColorTheme.Provider<Params, 'pdbe-structure-quality-report'> = {
     name: 'pdbe-structure-quality-report',
     name: 'pdbe-structure-quality-report',
     label: 'Structure Quality Report',
     label: 'Structure Quality Report',
     category: ColorTheme.Category.Validation,
     category: ColorTheme.Category.Validation,

+ 1 - 1
src/extensions/pdbe/structure-quality-report/prop.ts

@@ -73,7 +73,7 @@ namespace StructureQualityReport {
     }
     }
 
 
     export function fromCif(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): StructureQualityReport | undefined {
     export function fromCif(ctx: CustomProperty.Context, model: Model, props: StructureQualityReportProps): StructureQualityReport | undefined {
-        let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
+        const info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
         if (!info) return;
         if (!info) return;
         const data = getCifData(model);
         const data = getCifData(model);
         const issueMap = createIssueMapFromCif(model, data.residues, data.groups);
         const issueMap = createIssueMapFromCif(model, data.residues, data.groups);

+ 2 - 2
src/extensions/rcsb/assembly-symmetry/behavior.ts

@@ -47,7 +47,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
         }
         }
 
 
         update(p: { autoAttach: boolean }) {
         update(p: { autoAttach: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             return updated;
             return updated;
@@ -85,7 +85,7 @@ export const InitAssemblySymmetry3D = StateAction.build({
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
         await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
         await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
-    } catch(e) {
+    } catch (e) {
         plugin.log.error(`Assembly Symmetry: ${e}`);
         plugin.log.error(`Assembly Symmetry: ${e}`);
         return;
         return;
     }
     }

+ 1 - 1
src/extensions/rcsb/assembly-symmetry/prop.ts

@@ -130,7 +130,7 @@ export function getSymmetrySelectParam(structure?: Structure) {
             for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
             for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
                 const { symbol, kind } = assemblySymmetryData[i];
                 const { symbol, kind } = assemblySymmetryData[i];
                 if (symbol !== 'C1') {
                 if (symbol !== 'C1') {
-                    options.push([ i, `${i + 1}: ${symbol} ${kind}` ]);
+                    options.push([i, `${i + 1}: ${symbol} ${kind}`]);
                 }
                 }
             }
             }
             if (options.length > 1) {
             if (options.length > 1) {

+ 1 - 1
src/extensions/rcsb/assembly-symmetry/representation.ts

@@ -310,7 +310,7 @@ function setSymbolTransform(t: Mat4, symbol: string, axes: AssemblySymmetry.Rota
     }
     }
 }
 }
 
 
-const unitCircleDirections = (function() {
+const unitCircleDirections = (function () {
     const dirs: Vec3[] = [];
     const dirs: Vec3[] = [];
     const circle = polygon(12, false, 1);
     const circle = polygon(12, false, 1);
     for (let i = 0, il = circle.length; i < il; i += 3) {
     for (let i = 0, il = circle.length; i < il; i += 3) {

+ 1 - 1
src/extensions/rcsb/assembly-symmetry/ui.tsx

@@ -7,7 +7,7 @@
 import { CollapsableState, CollapsableControls } from '../../../mol-plugin-ui/base';
 import { CollapsableState, CollapsableControls } from '../../../mol-plugin-ui/base';
 import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
 import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
 import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry } from './behavior';
 import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry } from './behavior';
-import { AssemblySymmetryProvider,  AssemblySymmetryProps, AssemblySymmetryDataProvider, AssemblySymmetry } from './prop';
+import { AssemblySymmetryProvider, AssemblySymmetryProps, AssemblySymmetryDataProvider, AssemblySymmetry } from './prop';
 import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
 import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { StructureHierarchyManager } from '../../../mol-plugin-state/manager/structure/hierarchy';
 import { StructureHierarchyManager } from '../../../mol-plugin-state/manager/structure/hierarchy';

+ 10 - 9
src/extensions/rcsb/graphql/codegen.yml

@@ -1,12 +1,13 @@
 schema: https://data.rcsb.org/graphql
 schema: https://data.rcsb.org/graphql
 documents: './src/extensions/rcsb/graphql/symmetry.gql.ts'
 documents: './src/extensions/rcsb/graphql/symmetry.gql.ts'
 generates:
 generates:
-  './src/extensions/rcsb/graphql/types.ts':
-    plugins:
-      - add: '/* eslint-disable */'
-      - time
-      - typescript
-      - typescript-operations
-    config:
-      immutableTypes: true
-      skipTypename: true
+    './src/extensions/rcsb/graphql/types.ts':
+        plugins:
+            - add:
+                content: '/* eslint-disable */'
+            - time
+            - typescript
+            - typescript-operations
+        config:
+            immutableTypes: true
+            skipTypename: true

文件差异内容过多而无法显示
+ 2864 - 2772
src/extensions/rcsb/graphql/types.ts


+ 1 - 1
src/extensions/rcsb/validation-report/behavior.ts

@@ -63,7 +63,7 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         }
         }
 
 
         update(p: { autoAttach: boolean, showTooltip: boolean }) {
         update(p: { autoAttach: boolean, showTooltip: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.showTooltip = p.showTooltip;
             this.params.showTooltip = p.showTooltip;
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);

+ 6 - 6
src/extensions/rcsb/validation-report/prop.ts

@@ -112,7 +112,7 @@ namespace ValidationReport {
     }
     }
 
 
     export async function obtain(ctx: CustomProperty.Context, model: Model, props: ValidationReportProps): Promise<CustomProperty.Data<ValidationReport>> {
     export async function obtain(ctx: CustomProperty.Context, model: Model, props: ValidationReportProps): Promise<CustomProperty.Data<ValidationReport>> {
-        switch(props.source.name) {
+        switch (props.source.name) {
             case 'file': return open(ctx, model, props.source.params);
             case 'file': return open(ctx, model, props.source.params);
             case 'server': return fetch(ctx, model, props.source.params);
             case 'server': return fetch(ctx, model, props.source.params);
         }
         }
@@ -208,8 +208,8 @@ function createInterUnitClashes(structure: Structure, clashes: ValidationReport[
 
 
         for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) {
         for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) {
             // TODO create lookup
             // TODO create lookup
-            let indexA = SortedArray.indexOf(elementsA, a[i]);
-            let indexB = SortedArray.indexOf(elementsB, b[i]);
+            const indexA = SortedArray.indexOf(elementsA, a[i]);
+            const indexB = SortedArray.indexOf(elementsB, b[i]);
 
 
             if (indexA !== -1 && indexB !== -1) {
             if (indexA !== -1 && indexB !== -1) {
                 unitA.conformation.position(a[i], pA);
                 unitA.conformation.position(a[i], pA);
@@ -250,8 +250,8 @@ function createIntraUnitClashes(unit: Unit.Atomic, clashes: ValidationReport['cl
 
 
     for (let i = 0, il = edgeCount * 2; i < il; ++i) {
     for (let i = 0, il = edgeCount * 2; i < il; ++i) {
         // TODO create lookup
         // TODO create lookup
-        let indexA = SortedArray.indexOf(elements, a[i]);
-        let indexB = SortedArray.indexOf(elements, b[i]);
+        const indexA = SortedArray.indexOf(elements, a[i]);
+        const indexB = SortedArray.indexOf(elements, b[i]);
 
 
         if (indexA !== -1 && indexB !== -1) {
         if (indexA !== -1 && indexB !== -1) {
             unit.conformation.position(a[i], pA);
             unit.conformation.position(a[i], pA);
@@ -431,7 +431,7 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
 
 
     const groups = xml.getElementsByTagName('ModelledSubgroup');
     const groups = xml.getElementsByTagName('ModelledSubgroup');
     for (let i = 0, il = groups.length; i < il; ++i) {
     for (let i = 0, il = groups.length; i < il; ++i) {
-        const g = groups[ i ];
+        const g = groups[i];
         const ga = g.attributes;
         const ga = g.attributes;
 
 
         const pdbx_PDB_model_num = parseInt(getItem(ga, 'model'));
         const pdbx_PDB_model_num = parseInt(getItem(ga, 'model'));

+ 4 - 4
src/extensions/rcsb/validation-report/representation.ts

@@ -107,7 +107,7 @@ function getIntraClashLabel(structure: Structure, unit: Unit.Atomic, clashes: In
 
 
 function IntraClashLoci(structure: Structure, unit: Unit.Atomic, clashes: IntraUnitClashes, elements: number[]) {
 function IntraClashLoci(structure: Structure, unit: Unit.Atomic, clashes: IntraUnitClashes, elements: number[]) {
     return DataLoci('intra-clashes', { unit, clashes }, elements,
     return DataLoci('intra-clashes', { unit, clashes }, elements,
-        (boundingSphere: Sphere3D) =>  getIntraClashBoundingSphere(unit, clashes, elements, boundingSphere),
+        (boundingSphere: Sphere3D) => getIntraClashBoundingSphere(unit, clashes, elements, boundingSphere),
         () => getIntraClashLabel(structure, unit, clashes, elements));
         () => getIntraClashLabel(structure, unit, clashes, elements));
 }
 }
 
 
@@ -125,7 +125,7 @@ function getIntraClashLoci(pickingId: PickingId, structureGroup: StructureGroup,
 }
 }
 
 
 function eachIntraClash(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
 function eachIntraClash(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
-    let changed = false;
+    const changed = false;
     // TODO
     // TODO
     return changed;
     return changed;
 }
 }
@@ -226,7 +226,7 @@ function getInterClashLabel(structure: Structure, clashes: InterUnitClashes, ele
 
 
 function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
 function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
     return DataLoci('inter-clashes', clashes, elements,
     return DataLoci('inter-clashes', clashes, elements,
-        (boundingSphere: Sphere3D) =>  getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
+        (boundingSphere: Sphere3D) => getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
         () => getInterClashLabel(structure, clashes, elements));
         () => getInterClashLabel(structure, clashes, elements));
 }
 }
 
 
@@ -240,7 +240,7 @@ function getInterClashLoci(pickingId: PickingId, structure: Structure, id: numbe
 }
 }
 
 
 function eachInterClash(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
 function eachInterClash(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
-    let changed = false;
+    const changed = false;
     // TODO
     // TODO
     return changed;
     return changed;
 }
 }

+ 26 - 4
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();
@@ -34,7 +38,7 @@ class Camera implements ICamera {
     readonly inverseProjectionView: Mat4 = Mat4.identity();
     readonly inverseProjectionView: Mat4 = Mat4.identity();
 
 
     private pixelScale: number
     private pixelScale: number
-    get pixelRatio () {
+    get pixelRatio() {
         const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
         const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
         return dpr * this.pixelScale;
         return dpr * this.pixelScale;
     }
     }
@@ -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,

+ 17 - 19
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) {
-    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport;
+export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
+    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, proje
         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);
 }
 }

+ 22 - 14
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';
@@ -38,6 +38,7 @@ import { StereoCamera, StereoCameraParams } from './camera/stereo';
 import { Helper } from './helper/helper';
 import { Helper } from './helper/helper';
 import { Passes } from './passes/passes';
 import { Passes } from './passes/passes';
 import { shallowEqual } from '../mol-util';
 import { shallowEqual } from '../mol-util';
+import { MarkingParams } from './passes/marking';
 
 
 export const Canvas3DParams = {
 export const Canvas3DParams = {
     camera: PD.Group({
     camera: PD.Group({
@@ -80,8 +81,10 @@ export const Canvas3DParams = {
 
 
     multiSample: PD.Group(MultiSampleParams),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
     postprocessing: PD.Group(PostprocessingParams),
+    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),
 };
 };
@@ -113,23 +116,27 @@ 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');
 
 
-        const input = InputObserver.fromElement(canvas, { pixelScale });
+        const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
         const webgl = createContext(gl, { pixelScale });
         const webgl = createContext(gl, { pixelScale });
         const passes = new Passes(webgl, attribs);
         const passes = new Passes(webgl, attribs);
 
 
@@ -228,7 +235,7 @@ interface Canvas3D {
     /** Sets drawPaused = false without starting the built in animation loop */
     /** Sets drawPaused = false without starting the built in animation loop */
     resume(): void
     resume(): void
     identify(x: number, y: number): PickData | undefined
     identify(x: number, y: number): PickData | undefined
-    mark(loci: Representation.Loci, action: MarkerAction): void
+    mark(loci: Representation.Loci, action: MarkerAction, noDraw?: boolean): void
     getLoci(pickingId: PickingId | undefined): Representation.Loci
     getLoci(pickingId: PickingId | undefined): Representation.Loci
 
 
     notifyDidDraw: boolean,
     notifyDidDraw: boolean,
@@ -303,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;
@@ -337,7 +344,7 @@ namespace Canvas3D {
             return { loci, repr };
             return { loci, repr };
         }
         }
 
 
-        function mark(reprLoci: Representation.Loci, action: MarkerAction) {
+        function mark(reprLoci: Representation.Loci, action: MarkerAction, noDraw = false) {
             const { repr, loci } = reprLoci;
             const { repr, loci } = reprLoci;
             let changed = false;
             let changed = false;
             if (repr) {
             if (repr) {
@@ -347,7 +354,7 @@ namespace Canvas3D {
                 changed = helper.camera.mark(loci, action) || changed;
                 changed = helper.camera.mark(loci, action) || changed;
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
             }
             }
-            if (changed) {
+            if (changed && !noDraw) {
                 scene.update(void 0, true);
                 scene.update(void 0, true);
                 helper.handle.scene.update(void 0, true);
                 helper.handle.scene.update(void 0, true);
                 helper.camera.scene.update(void 0, true);
                 helper.camera.scene.update(void 0, true);
@@ -390,7 +397,7 @@ namespace Canvas3D {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } else {
                 } else {
-                    passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing);
+                    passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing, p.marking);
                 }
                 }
                 pickHelper.dirty = true;
                 pickHelper.dirty = true;
                 didRender = true;
                 didRender = true;
@@ -636,9 +643,11 @@ namespace Canvas3D {
                 viewport: p.viewport,
                 viewport: p.viewport,
 
 
                 postprocessing: { ...p.postprocessing },
                 postprocessing: { ...p.postprocessing },
+                marking: { ...p.marking },
                 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 },
             };
             };
@@ -729,7 +738,7 @@ namespace Canvas3D {
             resized,
             resized,
             setProps: (properties, doNotRequestDraw = false) => {
             setProps: (properties, doNotRequestDraw = false) => {
                 const props: PartialCanvas3DProps = typeof properties === 'function'
                 const props: PartialCanvas3DProps = typeof properties === 'function'
-                    ? produce(getProps(), properties)
+                    ? produce(getProps(), properties as any)
                     : properties;
                     : properties;
 
 
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
@@ -771,9 +780,11 @@ namespace Canvas3D {
                 }
                 }
 
 
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
+                if (props.marking) Object.assign(p.marking, props.marking);
                 if (props.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);
 
 
@@ -835,9 +846,6 @@ namespace Canvas3D {
                 height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
                 height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
                 y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
                 y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
                 width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
                 width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
-                // if (x + width >= gl.drawingBufferWidth) width = gl.drawingBufferWidth - x;
-                // if (y + height >= gl.drawingBufferHeight) height = gl.drawingBufferHeight - y - 1;
-                // console.log({ x, y, width, height });
             }
             }
 
 
             if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {
             if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {

+ 21 - 8
src/mol-canvas3d/controls/trackball.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>
@@ -10,7 +10,7 @@
 
 
 import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
 import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
 import { Viewport } from '../camera/util';
 import { Viewport } from '../camera/util';
-import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
+import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { Camera } from '../camera';
 import { Camera } from '../camera';
 import { absMax } from '../../mol-math/misc';
 import { absMax } from '../../mol-math/misc';
@@ -49,6 +49,9 @@ export const TrackballControlsParams = {
     minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
     minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
     maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
     maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
 
 
+    gestureScaleFactor: PD.Numeric(1, {}, { isHidden: true }),
+    maxWheelDelta: PD.Numeric(0.02, {}, { isHidden: true }),
+
     bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
     bindings: PD.Value(DefaultTrackballBindings, { isHidden: true }),
 
 
     /**
     /**
@@ -91,6 +94,7 @@ namespace TrackballControls {
         const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
         const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
         const wheelSub = input.wheel.subscribe(onWheel);
         const wheelSub = input.wheel.subscribe(onWheel);
         const pinchSub = input.pinch.subscribe(onPinch);
         const pinchSub = input.pinch.subscribe(onPinch);
+        const gestureSub = input.gesture.subscribe(onGesture);
 
 
         let _isInteracting = false;
         let _isInteracting = false;
 
 
@@ -390,25 +394,33 @@ namespace TrackballControls {
             _isInteracting = false;
             _isInteracting = false;
         }
         }
 
 
-        function onWheel({ x, y, dx, dy, dz, buttons, modifiers }: WheelInput) {
+        function onWheel({ x, y, spinX, spinY, dz, buttons, modifiers }: WheelInput) {
             if (outsideViewport(x, y)) return;
             if (outsideViewport(x, y)) return;
 
 
-            const delta = absMax(dx, dy, dz);
+            let delta = absMax(spinX * 0.075, spinY * 0.075, dz * 0.0001);
+            if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
+            else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
+
             if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
             if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
-                _zoomEnd[1] += delta * 0.0001;
+                _zoomEnd[1] += delta;
             }
             }
             if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
             if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
-                _focusEnd[1] += delta * 0.0001;
+                _focusEnd[1] += delta;
             }
             }
         }
         }
 
 
-        function onPinch({ fraction, buttons, modifiers }: PinchInput) {
+        function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
             if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
             if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
                 _isInteracting = true;
                 _isInteracting = true;
-                _zoomEnd[1] += (fraction - 1) * 0.1;
+                _zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
             }
             }
         }
         }
 
 
+        function onGesture({ deltaScale }: GestureInput) {
+            _isInteracting = true;
+            _zoomEnd[1] += p.gestureScaleFactor * deltaScale;
+        }
+
         function dispose() {
         function dispose() {
             if (disposed) return;
             if (disposed) return;
             disposed = true;
             disposed = true;
@@ -416,6 +428,7 @@ namespace TrackballControls {
             dragSub.unsubscribe();
             dragSub.unsubscribe();
             wheelSub.unsubscribe();
             wheelSub.unsubscribe();
             pinchSub.unsubscribe();
             pinchSub.unsubscribe();
+            gestureSub.unsubscribe();
             interactionEndSub.unsubscribe();
             interactionEndSub.unsubscribe();
         }
         }
 
 

+ 1 - 1
src/mol-canvas3d/helper/bounding-sphere-helper.ts

@@ -121,7 +121,7 @@ export class BoundingSphereHelper {
     }
     }
     get props() { return this._props as Readonly<DebugHelperProps>; }
     get props() { return this._props as Readonly<DebugHelperProps>; }
 
 
-    setProps (props: Partial<DebugHelperProps>) {
+    setProps(props: Partial<DebugHelperProps>) {
         Object.assign(this._props, props);
         Object.assign(this._props, props);
         if (this.isEnabled) this.update();
         if (this.isEnabled) this.update();
     }
     }

+ 50 - 8
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,18 +163,41 @@ 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) {
-        input.drag.subscribe(({x, y, buttons, button, modifiers }) => {
+    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 }) => {
             this.isInteracting = true;
             this.isInteracting = true;
             // console.log('drag');
             // console.log('drag');
             this.drag(x, y, buttons, button, modifiers);
             this.drag(x, y, buttons, button, modifiers);
         });
         });
 
 
-        input.move.subscribe(({x, y, inside, buttons, button, modifiers }) => {
+        input.move.subscribe(({ x, y, inside, buttons, button, modifiers }) => {
             if (!inside || this.isInteracting) return;
             if (!inside || this.isInteracting) return;
             // console.log('move');
             // console.log('move');
             this.move(x, y, buttons, button, modifiers);
             this.move(x, y, buttons, button, modifiers);
@@ -166,7 +208,7 @@ export class Canvas3dInteractionHelper {
             this.leave();
             this.leave();
         });
         });
 
 
-        input.click.subscribe(({x, y, buttons, button, modifiers }) => {
+        input.click.subscribe(({ x, y, buttons, button, modifiers }) => {
             if (this.outsideViewport(x, y)) return;
             if (this.outsideViewport(x, y)) return;
             // console.log('click');
             // console.log('click');
             this.click(x, y, buttons, button, modifiers);
             this.click(x, y, buttons, button, modifiers);

+ 30 - 29
src/mol-canvas3d/passes/draw.ts

@@ -22,10 +22,11 @@ import { Helper } from '../helper/helper';
 
 
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { depthMerge_frag } from '../../mol-gl/shader/depth-merge.frag';
 import { depthMerge_frag } from '../../mol-gl/shader/depth-merge.frag';
-import { copy_frag } from '../../mol-gl/shader/copy.frag';
 import { StereoCamera } from '../camera/stereo';
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
 import { WboitPass } from './wboit';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
+import { MarkingPass, MarkingProps } from './marking';
+import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
 
 
 const DepthMergeSchema = {
 const DepthMergeSchema = {
     ...QuadSchema,
     ...QuadSchema,
@@ -52,27 +53,6 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
     return createComputeRenderable(renderItem, values);
     return createComputeRenderable(renderItem, values);
 }
 }
 
 
-const CopySchema = {
-    ...QuadSchema,
-    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-    uTexSize: UniformSpec('v2'),
-};
-const  CopyShaderCode = ShaderCode('copy', quad_vert, copy_frag);
-type  CopyRenderable = ComputeRenderable<Values<typeof CopySchema>>
-
-function getCopyRenderable(ctx: WebGLContext, colorTexture: Texture): CopyRenderable {
-    const values: Values<typeof CopySchema> = {
-        ...QuadValues,
-        tColor: ValueCell.create(colorTexture),
-        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
-    };
-
-    const schema = { ...CopySchema };
-    const renderItem = createComputeRenderItem(ctx, 'triangles', CopyShaderCode, schema, values);
-
-    return createComputeRenderable(renderItem, values);
-}
-
 export class DrawPass {
 export class DrawPass {
     private readonly drawTarget: RenderTarget
     private readonly drawTarget: RenderTarget
 
 
@@ -92,6 +72,7 @@ export class DrawPass {
     private copyFboPostprocessing: CopyRenderable
     private copyFboPostprocessing: CopyRenderable
 
 
     private wboit: WboitPass | undefined
     private wboit: WboitPass | undefined
+    private readonly marking: MarkingPass
     readonly postprocessing: PostprocessingPass
     readonly postprocessing: PostprocessingPass
     private readonly antialiasing: AntialiasingPass
     private readonly antialiasing: AntialiasingPass
 
 
@@ -122,11 +103,12 @@ export class DrawPass {
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.marking = new MarkingPass(webgl, width, height);
         this.postprocessing = new PostprocessingPass(webgl, this);
         this.postprocessing = new PostprocessingPass(webgl, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
 
 
-        this.copyFboTarget = getCopyRenderable(webgl, this.colorTarget.texture);
-        this.copyFboPostprocessing = getCopyRenderable(webgl, this.postprocessing.target.texture);
+        this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
+        this.copyFboPostprocessing = createCopyRenderable(webgl, this.postprocessing.target.texture);
     }
     }
 
 
     reset() {
     reset() {
@@ -162,6 +144,7 @@ export class DrawPass {
                 this.wboit.setSize(width, height);
                 this.wboit.setSize(width, height);
             }
             }
 
 
+            this.marking.setSize(width, height);
             this.postprocessing.setSize(width, height);
             this.postprocessing.setSize(width, height);
             this.antialiasing.setSize(width, height);
             this.antialiasing.setSize(width, height);
         }
         }
@@ -281,10 +264,11 @@ export class DrawPass {
         renderer.renderBlendedTransparent(scene.primitives, camera, null);
         renderer.renderBlendedTransparent(scene.primitives, camera, null);
     }
     }
 
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) {
         const volumeRendering = scene.volumes.renderables.length > 0;
         const volumeRendering = scene.volumes.renderables.length > 0;
         const postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
         const postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
         const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
         const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
+        const markingEnabled = MarkingPass.isEnabled(markingProps);
 
 
         const { x, y, width, height } = camera.viewport;
         const { x, y, width, height } = camera.viewport;
         renderer.setViewport(x, y, width, height);
         renderer.setViewport(x, y, width, height);
@@ -309,6 +293,22 @@ export class DrawPass {
             this.drawTarget.bind();
             this.drawTarget.bind();
         }
         }
 
 
+        if (markingEnabled) {
+            const markingDepthTest = markingProps.ghostEdgeStrength < 1;
+            if (markingDepthTest) {
+                this.marking.depthTarget.bind();
+                renderer.clear(false);
+                renderer.renderMarkingDepth(scene.primitives, camera, null);
+            }
+
+            this.marking.maskTarget.bind();
+            renderer.clear(false);
+            renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
+
+            this.marking.update(markingProps);
+            this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+        }
+
         if (helper.debug.isEnabled) {
         if (helper.debug.isEnabled) {
             helper.debug.syncVisibility();
             helper.debug.syncVisibility();
             renderer.renderBlended(helper.debug.scene, camera, null);
             renderer.renderBlended(helper.debug.scene, camera, null);
@@ -338,15 +338,16 @@ export class DrawPass {
         this.webgl.gl.flush();
         this.webgl.gl.flush();
     }
     }
 
 
-    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+    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);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
         } else {
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
         }
         }
     }
     }
 
 

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

@@ -17,11 +17,13 @@ import { Viewport } from '../camera/util';
 import { PixelData } from '../../mol-util/image';
 import { PixelData } from '../../mol-util/image';
 import { Helper } from '../helper/helper';
 import { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
+import { MarkingParams } from './marking';
 
 
 export const ImageParams = {
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
     transparentBackground: PD.Boolean(false),
     multiSample: PD.Group(MultiSampleParams),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
     postprocessing: PD.Group(PostprocessingParams),
+    marking: PD.Group(MarkingParams),
 
 
     cameraHelper: PD.Group(CameraHelperParams),
     cameraHelper: PD.Group(CameraHelperParams),
 };
 };
@@ -85,7 +87,7 @@ export class ImagePass {
             this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this._colorTarget = this.multiSamplePass.colorTarget;
             this._colorTarget = this.multiSamplePass.colorTarget;
         } else {
         } else {
-            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props.postprocessing);
+            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props.postprocessing, this.props.marking);
             this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
             this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
         }
         }
     }
     }

+ 194 - 0
src/mol-canvas3d/passes/marking.ts

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { Texture } from '../../mol-gl/webgl/texture';
+import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { ValueCell } from '../../mol-util';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { quad_vert } from '../../mol-gl/shader/quad.vert';
+import { overlay_frag } from '../../mol-gl/shader/marking/overlay.frag';
+import { Viewport } from '../camera/util';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
+import { Color } from '../../mol-util/color';
+import { edge_frag } from '../../mol-gl/shader/marking/edge.frag';
+
+export const MarkingParams = {
+    enabled: PD.Boolean(false),
+    highlightEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(1.0, 0.4, 0.6), 1.0)),
+    selectEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(0.2, 1.0, 0.1), 1.0)),
+    edgeScale: PD.Numeric(1, { min: 1, max: 3, step: 1 }, { description: 'Thickness of the edge.' }),
+    ghostEdgeStrength: PD.Numeric(0.3, { min: 0, max: 1, step: 0.1 }, { description: 'Opacity of the hidden edges that are covered by other geometry. When set to 1, one less geometry render pass is done.' }),
+    innerEdgeFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Factor to multiply the inner edge color with - for added contrast.' }),
+};
+export type MarkingProps = PD.Values<typeof MarkingParams>
+
+export class MarkingPass {
+    static isEnabled(props: MarkingProps) {
+        return props.enabled;
+    }
+
+    readonly depthTarget: RenderTarget
+    readonly maskTarget: RenderTarget
+    private readonly edgesTarget: RenderTarget
+
+    private readonly edge: EdgeRenderable
+    private readonly overlay: OverlayRenderable
+
+    constructor(private webgl: WebGLContext, width: number, height: number) {
+        this.depthTarget = webgl.createRenderTarget(width, height);
+        this.maskTarget = webgl.createRenderTarget(width, height);
+        this.edgesTarget = webgl.createRenderTarget(width, height);
+
+        this.edge = getEdgeRenderable(webgl, this.maskTarget.texture);
+        this.overlay = getOverlayRenderable(webgl, this.edgesTarget.texture);
+    }
+
+    private setEdgeState(viewport: Viewport) {
+        const { gl, state } = this.webgl;
+
+        state.enable(gl.SCISSOR_TEST);
+        state.enable(gl.BLEND);
+        state.blendFunc(gl.ONE, gl.ONE);
+        state.blendEquation(gl.FUNC_ADD);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+
+        state.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
+
+    private setOverlayState(viewport: Viewport) {
+        const { gl, state } = this.webgl;
+
+        state.enable(gl.SCISSOR_TEST);
+        state.enable(gl.BLEND);
+        state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+        state.blendEquation(gl.FUNC_ADD);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+    }
+
+    setSize(width: number, height: number) {
+        const w = this.depthTarget.getWidth();
+        const h = this.depthTarget.getHeight();
+
+        if (width !== w || height !== h) {
+            this.depthTarget.setSize(width, height);
+            this.maskTarget.setSize(width, height);
+            this.edgesTarget.setSize(width, height);
+
+            ValueCell.update(this.edge.values.uTexSizeInv, Vec2.set(this.edge.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+            ValueCell.update(this.overlay.values.uTexSizeInv, Vec2.set(this.overlay.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+        }
+    }
+
+    update(props: MarkingProps) {
+        const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength } = props;
+
+        const { values: edgeValues } = this.edge;
+        const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
+        if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
+            ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
+            this.edge.update();
+        }
+
+        const { values: overlayValues } = this.overlay;
+        ValueCell.update(overlayValues.uHighlightEdgeColor, Color.toVec3Normalized(overlayValues.uHighlightEdgeColor.ref.value, highlightEdgeColor));
+        ValueCell.update(overlayValues.uSelectEdgeColor, Color.toVec3Normalized(overlayValues.uSelectEdgeColor.ref.value, selectEdgeColor));
+        ValueCell.update(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
+        ValueCell.update(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
+    }
+
+    render(viewport: Viewport, target: RenderTarget | undefined) {
+        this.edgesTarget.bind();
+        this.setEdgeState(viewport);
+        this.edge.render();
+
+        if (target) {
+            target.bind();
+        } else {
+            this.webgl.unbindFramebuffer();
+        }
+        this.setOverlayState(viewport);
+        this.overlay.render();
+    }
+}
+
+//
+
+const EdgeSchema = {
+    ...QuadSchema,
+    tMaskTexture: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uTexSizeInv: UniformSpec('v2'),
+    dEdgeScale: DefineSpec('number'),
+};
+const EdgeShaderCode = ShaderCode('edge', quad_vert, edge_frag);
+type EdgeRenderable = ComputeRenderable<Values<typeof EdgeSchema>>
+
+function getEdgeRenderable(ctx: WebGLContext, maskTexture: Texture): EdgeRenderable {
+    const width = maskTexture.getWidth();
+    const height = maskTexture.getHeight();
+
+    const values: Values<typeof EdgeSchema> = {
+        ...QuadValues,
+        tMaskTexture: ValueCell.create(maskTexture),
+        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+        dEdgeScale: ValueCell.create(1),
+    };
+
+    const schema = { ...EdgeSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', EdgeShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+//
+
+const OverlaySchema = {
+    ...QuadSchema,
+    tEdgeTexture: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uTexSizeInv: UniformSpec('v2'),
+    uHighlightEdgeColor: UniformSpec('v3'),
+    uSelectEdgeColor: UniformSpec('v3'),
+    uGhostEdgeStrength: UniformSpec('f'),
+    uInnerEdgeFactor: UniformSpec('f'),
+};
+const OverlayShaderCode = ShaderCode('overlay', quad_vert, overlay_frag);
+type OverlayRenderable = ComputeRenderable<Values<typeof OverlaySchema>>
+
+function getOverlayRenderable(ctx: WebGLContext, edgeTexture: Texture): OverlayRenderable {
+    const width = edgeTexture.getWidth();
+    const height = edgeTexture.getHeight();
+
+    const values: Values<typeof OverlaySchema> = {
+        ...QuadValues,
+        tEdgeTexture: ValueCell.create(edgeTexture),
+        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+        uHighlightEdgeColor: ValueCell.create(Vec3()),
+        uSelectEdgeColor: ValueCell.create(Vec3()),
+        uGhostEdgeStrength: ValueCell.create(0),
+        uInnerEdgeFactor: ValueCell.create(0),
+    };
+
+    const schema = { ...OverlaySchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', OverlayShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}

+ 29 - 25
src/mol-canvas3d/passes/multi-sample.ts

@@ -22,9 +22,9 @@ import { Renderer } from '../../mol-gl/renderer';
 import { Scene } from '../../mol-gl/scene';
 import { Scene } from '../../mol-gl/scene';
 import { Helper } from '../helper/helper';
 import { Helper } from '../helper/helper';
 import { StereoCamera } from '../camera/stereo';
 import { StereoCamera } from '../camera/stereo';
-
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { compose_frag } from '../../mol-gl/shader/compose.frag';
 import { compose_frag } from '../../mol-gl/shader/compose.frag';
+import { MarkingProps } from './marking';
 
 
 const ComposeSchema = {
 const ComposeSchema = {
     ...QuadSchema,
     ...QuadSchema,
@@ -55,7 +55,11 @@ export const MultiSampleParams = {
 };
 };
 export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
 export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
 
 
-type Props = { multiSample: MultiSampleProps, postprocessing: PostprocessingProps }
+type Props = {
+    multiSample: MultiSampleProps
+    postprocessing: PostprocessingProps
+    marking: MarkingProps
+}
 
 
 export class MultiSamplePass {
 export class MultiSamplePass {
     static isEnabled(props: MultiSampleProps) {
     static isEnabled(props: MultiSampleProps) {
@@ -119,7 +123,7 @@ export class MultiSamplePass {
         //
         //
         // This manual approach to MSAA re-renders the scene once for
         // This manual approach to MSAA re-renders the scene once for
         // each sample with camera jitter and accumulates the results.
         // each sample with camera jitter and accumulates the results.
-        const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
+        const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
 
 
         const { x, y, width, height } = camera.viewport;
         const { x, y, width, height } = camera.viewport;
         const baseSampleWeight = 1.0 / offsetList.length;
         const baseSampleWeight = 1.0 / offsetList.length;
@@ -144,7 +148,7 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, sampleWeight);
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
 
             // render scene
             // render scene
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
 
 
             // compose rendered scene with compose target
             // compose rendered scene with compose target
             composeTarget.bind();
             composeTarget.bind();
@@ -186,7 +190,7 @@ export class MultiSamplePass {
         //
         //
         // This manual approach to MSAA re-renders the scene once for
         // This manual approach to MSAA re-renders the scene once for
         // each sample with camera jitter and accumulates the results.
         // each sample with camera jitter and accumulates the results.
-        const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
+        const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
 
 
         if (sampleIndex === -2 || sampleIndex >= offsetList.length) return -2;
         if (sampleIndex === -2 || sampleIndex >= offsetList.length) return -2;
 
 
@@ -194,7 +198,7 @@ export class MultiSamplePass {
         const sampleWeight = 1.0 / offsetList.length;
         const sampleWeight = 1.0 / offsetList.length;
 
 
         if (sampleIndex === -1) {
         if (sampleIndex === -1) {
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
             ValueCell.update(compose.values.uWeight, 1.0);
             ValueCell.update(compose.values.uWeight, 1.0);
             ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             compose.update();
             compose.update();
@@ -222,7 +226,7 @@ export class MultiSamplePass {
                 camera.update();
                 camera.update();
 
 
                 // render scene
                 // render scene
-                drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+                drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
 
 
                 // compose rendered scene with compose target
                 // compose rendered scene with compose target
                 composeTarget.bind();
                 composeTarget.bind();
@@ -240,7 +244,7 @@ export class MultiSamplePass {
                 compose.render();
                 compose.render();
 
 
                 sampleIndex += 1;
                 sampleIndex += 1;
-                if (sampleIndex >= offsetList.length ) break;
+                if (sampleIndex >= offsetList.length) break;
             }
             }
         }
         }
 
 
@@ -274,33 +278,33 @@ export class MultiSamplePass {
 
 
 const JitterVectors = [
 const JitterVectors = [
     [
     [
-        [ 0, 0 ]
+        [0, 0]
     ],
     ],
     [
     [
-        [ 4, 4 ], [ -4, -4 ]
+        [4, 4], [-4, -4]
     ],
     ],
     [
     [
-        [ -2, -6 ], [ 6, -2 ], [ -6, 2 ], [ 2, 6 ]
+        [-2, -6], [6, -2], [-6, 2], [2, 6]
     ],
     ],
     [
     [
-        [ 1, -3 ], [ -1, 3 ], [ 5, 1 ], [ -3, -5 ],
-        [ -5, 5 ], [ -7, -1 ], [ 3, 7 ], [ 7, -7 ]
+        [1, -3], [-1, 3], [5, 1], [-3, -5],
+        [-5, 5], [-7, -1], [3, 7], [7, -7]
     ],
     ],
     [
     [
-        [ 1, 1 ], [ -1, -3 ], [ -3, 2 ], [ 4, -1 ],
-        [ -5, -2 ], [ 2, 5 ], [ 5, 3 ], [ 3, -5 ],
-        [ -2, 6 ], [ 0, -7 ], [ -4, -6 ], [ -6, 4 ],
-        [ -8, 0 ], [ 7, -4 ], [ 6, 7 ], [ -7, -8 ]
+        [1, 1], [-1, -3], [-3, 2], [4, -1],
+        [-5, -2], [2, 5], [5, 3], [3, -5],
+        [-2, 6], [0, -7], [-4, -6], [-6, 4],
+        [-8, 0], [7, -4], [6, 7], [-7, -8]
     ],
     ],
     [
     [
-        [ -4, -7 ], [ -7, -5 ], [ -3, -5 ], [ -5, -4 ],
-        [ -1, -4 ], [ -2, -2 ], [ -6, -1 ], [ -4, 0 ],
-        [ -7, 1 ], [ -1, 2 ], [ -6, 3 ], [ -3, 3 ],
-        [ -7, 6 ], [ -3, 6 ], [ -5, 7 ], [ -1, 7 ],
-        [ 5, -7 ], [ 1, -6 ], [ 6, -5 ], [ 4, -4 ],
-        [ 2, -3 ], [ 7, -2 ], [ 1, -1 ], [ 4, -1 ],
-        [ 2, 1 ], [ 6, 2 ], [ 0, 4 ], [ 4, 4 ],
-        [ 2, 5 ], [ 7, 5 ], [ 5, 6 ], [ 3, 7 ]
+        [-4, -7], [-7, -5], [-3, -5], [-5, -4],
+        [-1, -4], [-2, -2], [-6, -1], [-4, 0],
+        [-7, 1], [-1, 2], [-6, 3], [-3, 3],
+        [-7, 6], [-3, 6], [-5, 7], [-1, 7],
+        [5, -7], [1, -6], [6, -5], [4, -4],
+        [2, -3], [7, -2], [1, -1], [4, -1],
+        [2, 1], [6, 2], [0, 4], [4, 4],
+        [2, 5], [7, 5], [5, 6], [3, 7]
     ]
     ]
 ];
 ];
 
 

+ 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);
     }
     }
 }
 }

+ 12 - 7
src/mol-canvas3d/passes/postprocessing.ts

@@ -154,10 +154,10 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
 }
 }
 
 
 function getBlurKernel(kernelSize: number): number[] {
 function getBlurKernel(kernelSize: number): number[] {
-    let sigma = kernelSize / 3.0;
-    let halfKernelSize = Math.floor((kernelSize + 1) / 2);
+    const sigma = kernelSize / 3.0;
+    const halfKernelSize = Math.floor((kernelSize + 1) / 2);
 
 
-    let kernel = [];
+    const kernel = [];
     for (let x = 0; x < halfKernelSize; x++) {
     for (let x = 0; x < halfKernelSize; x++) {
         kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
         kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
     }
     }
@@ -166,7 +166,7 @@ function getBlurKernel(kernelSize: number): number[] {
 }
 }
 
 
 function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
 function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
-    let samples = [];
+    const samples = [];
     for (let i = 0; i < nSamples; i++) {
     for (let i = 0; i < nSamples; i++) {
         let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
         let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
         scale = 0.1 + scale * (1.0 - 0.1);
         scale = 0.1 + scale * (1.0 - 0.1);
@@ -193,6 +193,7 @@ const PostprocessingSchema = {
     uFogNear: UniformSpec('f'),
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
     uFogColor: UniformSpec('v3'),
+    uOutlineColor: UniformSpec('v3'),
     uTransparentBackground: UniformSpec('b'),
     uTransparentBackground: UniformSpec('b'),
 
 
     uMaxPossibleViewZDiff: UniformSpec('f'),
     uMaxPossibleViewZDiff: UniformSpec('f'),
@@ -220,6 +221,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uFogNear: ValueCell.create(10000),
         uFogNear: ValueCell.create(10000),
         uFogFar: ValueCell.create(10000),
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
+        uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
         uTransparentBackground: ValueCell.create(false),
         uTransparentBackground: ValueCell.create(false),
 
 
         uMaxPossibleViewZDiff: ValueCell.create(0.5),
         uMaxPossibleViewZDiff: ValueCell.create(0.5),
@@ -241,7 +243,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
 export const PostprocessingParams = {
 export const PostprocessingParams = {
     occlusion: PD.MappedStatic('on', {
     occlusion: PD.MappedStatic('on', {
         on: PD.Group({
         on: PD.Group({
-            samples: PD.Numeric(32, {min: 1, max: 256, step: 1}),
+            samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
             radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final radius is 2^x.' }),
             radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final radius is 2^x.' }),
             bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
             bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
             blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
             blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
@@ -252,6 +254,7 @@ export const PostprocessingParams = {
         on: PD.Group({
         on: PD.Group({
             scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
             scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
             threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
             threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
+            color: PD.Color(Color(0x000000)),
         }),
         }),
         off: PD.Group({})
         off: PD.Group({})
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -314,7 +317,7 @@ export class PostprocessingPass {
 
 
         this.randomHemisphereVector = [];
         this.randomHemisphereVector = [];
         for (let i = 0; i < 256; i++) {
         for (let i = 0; i < 256; i++) {
-            let v = Vec3();
+            const v = Vec3();
             v[0] = Math.random() * 2.0 - 1.0;
             v[0] = Math.random() * 2.0 - 1.0;
             v[1] = Math.random() * 2.0 - 1.0;
             v[1] = Math.random() * 2.0 - 1.0;
             v[2] = Math.random();
             v[2] = Math.random();
@@ -376,7 +379,7 @@ export class PostprocessingPass {
         const outlinesEnabled = props.outline.name === 'on';
         const outlinesEnabled = props.outline.name === 'on';
         const occlusionEnabled = props.occlusion.name === 'on';
         const occlusionEnabled = props.occlusion.name === 'on';
 
 
-        let invProjection = Mat4.identity();
+        const invProjection = Mat4.identity();
         Mat4.invert(invProjection, camera.projection);
         Mat4.invert(invProjection, camera.projection);
 
 
         if (props.occlusion.name === 'on') {
         if (props.occlusion.name === 'on') {
@@ -446,6 +449,8 @@ export class PostprocessingPass {
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
 
 
+            ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
+
             ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
             ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
             if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
             if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
             ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
             ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);

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

@@ -25,8 +25,8 @@ import { Viewport } from '../camera/util';
 import { isDebugMode } from '../../mol-util/debug';
 import { isDebugMode } from '../../mol-util/debug';
 
 
 export const SmaaParams = {
 export const SmaaParams = {
-    edgeThreshold:PD.Numeric(0.1, { min: 0.05, max: 0.15, step: 0.01 }),
-    maxSearchSteps:PD.Numeric(16, { min: 0, max: 32, step: 1 }),
+    edgeThreshold: PD.Numeric(0.1, { min: 0.05, max: 0.15, step: 0.01 }),
+    maxSearchSteps: PD.Numeric(16, { min: 0, max: 32, step: 1 }),
 };
 };
 export type SmaaProps = PD.Values<typeof SmaaParams>
 export type SmaaProps = PD.Values<typeof SmaaParams>
 
 

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

@@ -12,7 +12,7 @@ export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height:
 }
 }
 
 
 /** Resize canvas to container element taking `devicePixelRatio` into account */
 /** Resize canvas to container element taking `devicePixelRatio` into account */
-export function resizeCanvas (canvas: HTMLCanvasElement, container: HTMLElement, scale = 1) {
+export function resizeCanvas(canvas: HTMLCanvasElement, container: HTMLElement, scale = 1) {
     let width = window.innerWidth;
     let width = window.innerWidth;
     let height = window.innerHeight;
     let height = window.innerHeight;
     if (container !== document.body) {
     if (container !== document.body) {

+ 1 - 1
src/mol-data/db/_spec/table.spec.ts

@@ -100,7 +100,7 @@ describe('table', () => {
             n: Column.ofArray({ array: ['row1', 'row2'], schema: Column.Schema.str }),
             n: Column.ofArray({ array: ['row1', 'row2'], schema: Column.Schema.str }),
         });
         });
         const s = { x: Column.Schema.int, y: Column.Schema.int };
         const s = { x: Column.Schema.int, y: Column.Schema.int };
-        const picked = Table.pickColumns(s, t, { y: Column.ofArray({ array: [3, 4], schema: Column.Schema.int })});
+        const picked = Table.pickColumns(s, t, { y: Column.ofArray({ array: [3, 4], schema: Column.Schema.int }) });
         expect(picked._columns).toEqual(['x', 'y']);
         expect(picked._columns).toEqual(['x', 'y']);
         expect(picked._rowCount).toEqual(2);
         expect(picked._rowCount).toEqual(2);
         expect(picked.x.toArray()).toEqual([10, -1]);
         expect(picked.x.toArray()).toEqual([10, -1]);

+ 4 - 4
src/mol-data/generic/_spec/linked-list.spec.ts

@@ -33,21 +33,21 @@ describe('linked list', () => {
         expect(list.count).toBe(5);
         expect(list.count).toBe(5);
     });
     });
 
 
-    it ('remove', () => {
+    it('remove', () => {
         const list = create([1, 2, 3, 4]);
         const list = create([1, 2, 3, 4]);
-        let fst = list.removeFirst();
+        const fst = list.removeFirst();
         expect(fst).toBe(1);
         expect(fst).toBe(1);
         expect(list.last!.value).toBe(4);
         expect(list.last!.value).toBe(4);
         expect(list.count).toBe(3);
         expect(list.count).toBe(3);
         expect(toArray(list)).toEqual([2, 3, 4]);
         expect(toArray(list)).toEqual([2, 3, 4]);
 
 
-        let last = list.removeLast();
+        const last = list.removeLast();
         expect(last).toBe(4);
         expect(last).toBe(4);
         expect(list.last!.value).toBe(3);
         expect(list.last!.value).toBe(3);
         expect(list.count).toBe(2);
         expect(list.count).toBe(2);
         expect(toArray(list)).toEqual([2, 3]);
         expect(toArray(list)).toEqual([2, 3]);
 
 
-        let n3 = list.find(3)!;
+        const n3 = list.find(3)!;
         list.remove(n3);
         list.remove(n3);
         expect(list.first!.value).toBe(2);
         expect(list.first!.value).toBe(2);
         expect(list.last!.value).toBe(2);
         expect(list.last!.value).toBe(2);

+ 1 - 1
src/mol-data/int/impl/interval.ts

@@ -16,7 +16,7 @@ export const start = Tuple.fst;
 export const end = Tuple.snd;
 export const end = Tuple.snd;
 export const min = Tuple.fst;
 export const min = Tuple.fst;
 export function max(i: Tuple) { return Tuple.snd(i) - 1; }
 export function max(i: Tuple) { return Tuple.snd(i) - 1; }
-export function size(i: Tuple) { return Tuple.snd(i) - Tuple.fst(i); }
+export const size = Tuple.diff;
 export const hashCode = Tuple.hashCode;
 export const hashCode = Tuple.hashCode;
 export const toString = Tuple.toString;
 export const toString = Tuple.toString;
 
 

+ 1 - 1
src/mol-data/int/impl/ordered-set.ts

@@ -19,7 +19,7 @@ export const ofBounds = I.ofBounds;
 export function ofSortedArray(xs: Nums): OrderedSetImpl {
 export function ofSortedArray(xs: Nums): OrderedSetImpl {
     if (!xs.length) return Empty;
     if (!xs.length) return Empty;
     // check if the array is just a range
     // check if the array is just a range
-    if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return I.ofRange(xs[0], xs[xs.length - 1]);
+    if (S.isRange(xs)) return I.ofRange(xs[0], xs[xs.length - 1]);
     return xs as any;
     return xs as any;
 }
 }
 
 

+ 7 - 4
src/mol-data/int/impl/sorted-array.ts

@@ -22,9 +22,10 @@ export function ofRange(min: number, max: number) {
     return ret;
     return ret;
 }
 }
 export function is(xs: any): xs is Nums { return xs && (Array.isArray(xs) || !!xs.buffer); }
 export function is(xs: any): xs is Nums { return xs && (Array.isArray(xs) || !!xs.buffer); }
+export function isRange(xs: Nums) { return xs[xs.length - 1] - xs[0] + 1 === xs.length; }
 
 
 export function start(xs: Nums) { return xs[0]; }
 export function start(xs: Nums) { return xs[0]; }
-export function end(xs: Nums) { return xs[xs.length - 1] + 1;  }
+export function end(xs: Nums) { return xs[xs.length - 1] + 1; }
 export function min(xs: Nums) { return xs[0]; }
 export function min(xs: Nums) { return xs[0]; }
 export function max(xs: Nums) { return xs[xs.length - 1]; }
 export function max(xs: Nums) { return xs[xs.length - 1]; }
 export function size(xs: Nums) { return xs.length; }
 export function size(xs: Nums) { return xs.length; }
@@ -59,9 +60,11 @@ export function getAt(xs: Nums, i: number) { return xs[i]; }
 
 
 export function areEqual(a: Nums, b: Nums) {
 export function areEqual(a: Nums, b: Nums) {
     if (a === b) return true;
     if (a === b) return true;
-    const aSize = a.length;
+    let aSize = a.length;
     if (aSize !== b.length || a[0] !== b[0] || a[aSize - 1] !== b[aSize - 1]) return false;
     if (aSize !== b.length || a[0] !== b[0] || a[aSize - 1] !== b[aSize - 1]) return false;
-    for (let i = 0; i < aSize; i++) {
+    if (isRange(a)) return true;
+    aSize--;
+    for (let i = 1; i < aSize; i++) {
         if (a[i] !== b[i]) return false;
         if (a[i] !== b[i]) return false;
     }
     }
     return true;
     return true;
@@ -340,7 +343,7 @@ export function deduplicate(xs: Nums) {
 }
 }
 
 
 export function indicesOf(a: Nums, b: Nums): Nums {
 export function indicesOf(a: Nums, b: Nums): Nums {
-    if (a === b) return ofSortedArray(createRangeArray(0, a.length - 1));
+    if (areEqual(a, b)) return ofSortedArray(createRangeArray(0, a.length - 1));
 
 
     const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
     const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
     let i = sI, j = sJ;
     let i = sI, j = sJ;

+ 1 - 0
src/mol-data/int/sorted-array.ts

@@ -17,6 +17,7 @@ namespace SortedArray {
     /** create sorted array [min, max) (it does NOT contain the max value) */
     /** create sorted array [min, max) (it does NOT contain the max value) */
     export const ofBounds: <T extends number = number>(min: T, max: T) => SortedArray<T> = (min, max) => Impl.ofRange(min, max - 1) as any;
     export const ofBounds: <T extends number = number>(min: T, max: T) => SortedArray<T> = (min, max) => Impl.ofRange(min, max - 1) as any;
     export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
     export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
+    export const isRange: <T extends number = number>(array: ArrayLike<number>) => boolean = Impl.isRange as any;
 
 
     export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
     export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
     /** Returns the index of `x` in `set` or -1 if not found. */
     /** Returns the index of `x` in `set` or -1 if not found. */

+ 7 - 1
src/mol-data/int/tuple.ts

@@ -15,7 +15,7 @@ interface IntTuple { '@type': 'int-tuple' }
 namespace IntTuple {
 namespace IntTuple {
     export const Zero: IntTuple = 0 as any;
     export const Zero: IntTuple = 0 as any;
 
 
-    const { _int32, _float64, _int32_1, _float64_1 } = (function() {
+    const { _int32, _float64, _int32_1, _float64_1 } = (function () {
         const data = new ArrayBuffer(8);
         const data = new ArrayBuffer(8);
         const data_1 = new ArrayBuffer(8);
         const data_1 = new ArrayBuffer(8);
         return {
         return {
@@ -36,6 +36,12 @@ namespace IntTuple {
         return _float64[0] as any;
         return _float64[0] as any;
     }
     }
 
 
+    /** snd - fst */
+    export function diff(t: IntTuple) {
+        _float64[0] = t as any;
+        return _int32[1] - _int32[0];
+    }
+
     export function fst(t: IntTuple): number {
     export function fst(t: IntTuple): number {
         _float64[0] = t as any;
         _float64[0] = t as any;
         return _int32[0];
         return _int32[0];

+ 5 - 5
src/mol-data/util/_spec/chunked-array.spec.ts

@@ -8,14 +8,14 @@ import { ChunkedArray } from '../chunked-array';
 
 
 describe('Chunked Array', () => {
 describe('Chunked Array', () => {
     it('creation', () => {
     it('creation', () => {
-        const arr  = ChunkedArray.create<number, 2>(Array, 2, 2);
+        const arr = ChunkedArray.create<number, 2>(Array, 2, 2);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 3, 4);
         ChunkedArray.add2(arr, 3, 4);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
     });
     });
 
 
     it('initial', () => {
     it('initial', () => {
-        const arr  = ChunkedArray.create(Int32Array, 2, 6, new Int32Array([1, 2, 3, 4]));
+        const arr = ChunkedArray.create(Int32Array, 2, 6, new Int32Array([1, 2, 3, 4]));
         ChunkedArray.add2(arr, 4, 3);
         ChunkedArray.add2(arr, 4, 3);
         ChunkedArray.add2(arr, 2, 1);
         ChunkedArray.add2(arr, 2, 1);
         ChunkedArray.add2(arr, 5, 6);
         ChunkedArray.add2(arr, 5, 6);
@@ -23,13 +23,13 @@ describe('Chunked Array', () => {
     });
     });
 
 
     it('add many', () => {
     it('add many', () => {
-        const arr  = ChunkedArray.create<number, 2>(Array, 2, 2);
+        const arr = ChunkedArray.create<number, 2>(Array, 2, 2);
         ChunkedArray.addMany(arr, [1, 2, 3, 4]);
         ChunkedArray.addMany(arr, [1, 2, 3, 4]);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
     });
     });
 
 
     it('resize', () => {
     it('resize', () => {
-        const arr  = ChunkedArray.create<number, 2>(Int32Array, 2, 2);
+        const arr = ChunkedArray.create<number, 2>(Int32Array, 2, 2);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 3, 4);
         ChunkedArray.add2(arr, 3, 4);
         ChunkedArray.add2(arr, 5, 6);
         ChunkedArray.add2(arr, 5, 6);
@@ -39,7 +39,7 @@ describe('Chunked Array', () => {
     });
     });
 
 
     it('resize-fraction', () => {
     it('resize-fraction', () => {
-        const arr  = ChunkedArray.create<number, 2>(Int32Array, 2, 2.5);
+        const arr = ChunkedArray.create<number, 2>(Int32Array, 2, 2.5);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 1, 2);
         ChunkedArray.add2(arr, 3, 4);
         ChunkedArray.add2(arr, 3, 4);
         ChunkedArray.add2(arr, 5, 6);
         ChunkedArray.add2(arr, 5, 6);

+ 1 - 1
src/mol-data/util/chunked-array.ts

@@ -36,7 +36,7 @@ namespace ChunkedArray {
     }
     }
 
 
     function allocateNext(array: ChunkedArray<any, any>) {
     function allocateNext(array: ChunkedArray<any, any>) {
-        let nextSize = array.growBy * array.elementSize;
+        const nextSize = array.growBy * array.elementSize;
         array.currentSize = nextSize;
         array.currentSize = nextSize;
         array.currentIndex = 0;
         array.currentIndex = 0;
         array.currentChunk = new array.ctor(nextSize);
         array.currentChunk = new array.ctor(nextSize);

+ 10 - 1
src/mol-data/util/hash-functions.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2017-2018 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>
@@ -70,6 +70,15 @@ export function sortedCantorPairing(a: number, b: number) {
     return a < b ? cantorPairing(a, b) : cantorPairing(b, a);
     return a < b ? cantorPairing(a, b) : cantorPairing(b, a);
 }
 }
 
 
+export function invertCantorPairing(out: [number, number], z: number) {
+    const w = Math.floor((Math.sqrt(8 * z + 1) - 1) / 2);
+    const t = (w * w + w) / 2;
+    const y = z - t;
+    out[0] = w - y;
+    out[1] = y;
+    return out;
+}
+
 /**
 /**
  * 32 bit FNV-1a hash, see http://isthe.com/chongo/tech/comp/fnv/
  * 32 bit FNV-1a hash, see http://isthe.com/chongo/tech/comp/fnv/
  */
  */

+ 53 - 8
src/mol-geo/geometry/base.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>
  */
  */
@@ -15,6 +15,8 @@ import { ColorNames } from '../../mol-util/color/names';
 import { NullLocation } from '../../mol-model/location';
 import { NullLocation } from '../../mol-model/location';
 import { UniformColorTheme } from '../../mol-theme/color/uniform';
 import { UniformColorTheme } from '../../mol-theme/color/uniform';
 import { UniformSizeTheme } from '../../mol-theme/size/uniform';
 import { UniformSizeTheme } from '../../mol-theme/size/uniform';
+import { smoothstep } from '../../mol-math/interpolate';
+import { Material } from '../../mol-util/material';
 
 
 export const VisualQualityInfo = {
 export const VisualQualityInfo = {
     'custom': {},
     'custom': {},
@@ -33,27 +35,63 @@ export const VisualQualityOptions = PD.arrayToOptions(VisualQualityNames);
 
 
 //
 //
 
 
-export namespace BaseGeometry {
-    export const Params = {
-        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity', isEssential: true, description: 'How opaque/transparent the representation is rendered.' }),
-        quality: PD.Select<VisualQuality>('auto', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }),
+export const ColorSmoothingParams = {
+    smoothColors: PD.MappedStatic('auto', {
+        auto: PD.Group({}),
+        on: PD.Group({
+            resolutionFactor: PD.Numeric(2, { min: 0.5, max: 6, step: 0.1 }),
+            sampleStride: PD.Numeric(3, { min: 1, max: 12, step: 1 }),
+        }),
+        off: PD.Group({})
+    }),
+};
+export type ColorSmoothingParams = typeof ColorSmoothingParams
+
+export function hasColorSmoothingProp(props: PD.Values<any>): props is PD.Values<ColorSmoothingParams> {
+    return !!props.smoothColors;
+}
+
+export function getColorSmoothingProps(smoothColors: PD.Values<ColorSmoothingParams>['smoothColors'], preferSmoothing?: boolean, resolution?: number) {
+    if ((smoothColors.name === 'on' || (smoothColors.name === 'auto' && preferSmoothing)) && resolution && resolution < 3) {
+        let stride = 3;
+        if (smoothColors.name === 'on') {
+            resolution *= smoothColors.params.resolutionFactor;
+            stride = smoothColors.params.sampleStride;
+        } else {
+            // https://graphtoy.com/?f1(x,t)=(2-smoothstep(0,1.1,x))*x&coords=0.7,0.6,1.8
+            resolution *= 2 - smoothstep(0, 1.1, resolution);
+            resolution = Math.max(0.5, resolution);
+            if (resolution > 1.2) stride = 2;
+        }
+        return { resolution, stride };
     };
     };
-    export type Params = typeof Params
+}
 
 
+//
+
+export namespace BaseGeometry {
+    export const MaterialCategory: PD.Info = { category: 'Material' };
     export const ShadingCategory: PD.Info = { category: 'Shading' };
     export const ShadingCategory: PD.Info = { category: 'Shading' };
     export const CustomQualityParamInfo: PD.Info = {
     export const CustomQualityParamInfo: PD.Info = {
         category: 'Custom Quality',
         category: 'Custom Quality',
         hideIf: (params: PD.Values<Params>) => typeof params.quality !== 'undefined' && params.quality !== 'custom'
         hideIf: (params: PD.Values<Params>) => typeof params.quality !== 'undefined' && params.quality !== 'custom'
     };
     };
 
 
+    export const Params = {
+        alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }, { label: 'Opacity', isEssential: true, description: 'How opaque/transparent the representation is rendered.' }),
+        quality: PD.Select<VisualQuality>('auto', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }),
+        material: Material.getParam(),
+    };
+    export type Params = typeof Params
+
     export type Counts = { drawCount: number, vertexCount: number, groupCount: number, instanceCount: number }
     export type Counts = { drawCount: number, vertexCount: number, groupCount: number, instanceCount: number }
 
 
     export function createSimple(colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData) {
     export function createSimple(colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData) {
         if (!transform) transform = createIdentityTransform();
         if (!transform) transform = createIdentityTransform();
         const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
         const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
         const theme: Theme = {
         const theme: Theme = {
-            color: UniformColorTheme({}, { value: colorValue}),
-            size: UniformSizeTheme({}, { value: sizeValue})
+            color: UniformColorTheme({}, { value: colorValue }),
+            size: UniformSizeTheme({}, { value: sizeValue })
         };
         };
         return { transform, locationIterator, theme };
         return { transform, locationIterator, theme };
     }
     }
@@ -65,11 +103,18 @@ export namespace BaseGeometry {
             uVertexCount: ValueCell.create(counts.vertexCount),
             uVertexCount: ValueCell.create(counts.vertexCount),
             uGroupCount: ValueCell.create(counts.groupCount),
             uGroupCount: ValueCell.create(counts.groupCount),
             drawCount: ValueCell.create(counts.drawCount),
             drawCount: ValueCell.create(counts.drawCount),
+            uMetalness: ValueCell.create(props.material.metalness),
+            uRoughness: ValueCell.create(props.material.roughness),
+            uBumpiness: ValueCell.create(props.material.bumpiness),
+            dLightCount: ValueCell.create(1),
         };
         };
     }
     }
 
 
     export function updateValues(values: BaseValues, props: PD.Values<Params>) {
     export function updateValues(values: BaseValues, props: PD.Values<Params>) {
         ValueCell.updateIfChanged(values.alpha, props.alpha); // `uAlpha` is set in renderable.render
         ValueCell.updateIfChanged(values.alpha, props.alpha); // `uAlpha` is set in renderable.render
+        ValueCell.updateIfChanged(values.uMetalness, props.material.metalness);
+        ValueCell.updateIfChanged(values.uRoughness, props.material.roughness);
+        ValueCell.updateIfChanged(values.uBumpiness, props.material.bumpiness);
     }
     }
 
 
     export function createRenderableState(props: Partial<PD.Values<Params>> = {}): RenderableState {
     export function createRenderableState(props: Partial<PD.Values<Params>> = {}): RenderableState {

+ 10 - 1
src/mol-geo/geometry/cylinders/cylinders.ts

@@ -6,7 +6,7 @@
 
 
 import { ValueCell } from '../../../mol-util';
 import { ValueCell } from '../../../mol-util';
 import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
-import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
+import { transformPositionArray, GroupMapping, createGroupMapping } from '../../util';
 import { GeometryUtils } from '../geometry';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
 import { createMarkers } from '../marker-data';
@@ -25,6 +25,7 @@ import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
 import { createEmptyClipping } from '../clipping-data';
 import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
 import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
 import { RenderableState } from '../../../mol-gl/renderable';
 import { RenderableState } from '../../../mol-gl/renderable';
+import { createEmptySubstance } from '../substance-data';
 
 
 export interface Cylinders {
 export interface Cylinders {
     readonly kind: 'cylinders',
     readonly kind: 'cylinders',
@@ -156,6 +157,8 @@ export namespace Cylinders {
         doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
         doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
         ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
         ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
         xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
         xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
+        bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
+        bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
     };
     };
     export type Params = typeof Params
     export type Params = typeof Params
 
 
@@ -200,6 +203,7 @@ export namespace Cylinders {
         const marker = createMarkers(instanceCount * groupCount);
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
         const clipping = createEmptyClipping();
 
 
         const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
         const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
@@ -224,6 +228,7 @@ export namespace Cylinders {
             ...marker,
             ...marker,
             ...overpaint,
             ...overpaint,
             ...transparency,
             ...transparency,
+            ...material,
             ...clipping,
             ...clipping,
             ...transform,
             ...transform,
 
 
@@ -234,6 +239,8 @@ export namespace Cylinders {
             dDoubleSided: ValueCell.create(props.doubleSided),
             dDoubleSided: ValueCell.create(props.doubleSided),
             dIgnoreLight: ValueCell.create(props.ignoreLight),
             dIgnoreLight: ValueCell.create(props.ignoreLight),
             dXrayShaded: ValueCell.create(props.xrayShaded),
             dXrayShaded: ValueCell.create(props.xrayShaded),
+            uBumpFrequency: ValueCell.create(props.bumpFrequency),
+            uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
         };
         };
     }
     }
 
 
@@ -249,6 +256,8 @@ export namespace Cylinders {
         ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
         ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
         ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
         ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
         ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
         ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
+        ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
+        ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
     }
     }
 
 
     function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {
     function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {

+ 14 - 4
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';
@@ -28,6 +28,7 @@ import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './
 import { createEmptyClipping } from '../clipping-data';
 import { createEmptyClipping } from '../clipping-data';
 import { Grid, Volume } from '../../../mol-model/volume';
 import { Grid, Volume } from '../../../mol-model/volume';
 import { ColorNames } from '../../../mol-util/color/names';
 import { ColorNames } from '../../../mol-util/color/names';
+import { createEmptySubstance } from '../substance-data';
 
 
 const VolumeBox = Box();
 const VolumeBox = Box();
 
 
@@ -129,7 +130,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']) {
@@ -238,6 +247,7 @@ export namespace DirectVolume {
         const marker = createMarkers(instanceCount * groupCount);
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
         const clipping = createEmptyClipping();
 
 
         const [x, y, z] = gridDimension.ref.value;
         const [x, y, z] = gridDimension.ref.value;
@@ -262,6 +272,7 @@ export namespace DirectVolume {
             ...marker,
             ...marker,
             ...overpaint,
             ...overpaint,
             ...transparency,
             ...transparency,
+            ...material,
             ...clipping,
             ...clipping,
             ...transform,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
             ...BaseGeometry.createValues(props, counts),
@@ -311,8 +322,7 @@ export namespace DirectVolume {
     }
     }
 
 
     function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
     function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
-        ValueCell.updateIfChanged(values.alpha, props.alpha);
-        ValueCell.updateIfChanged(values.uAlpha, props.alpha);
+        BaseGeometry.updateValues(values, props);
         ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
         ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
         ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
         ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
         ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
         ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);

+ 8 - 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';
@@ -26,6 +26,7 @@ import { fillSerial } from '../../../mol-util/array';
 import { createEmptyClipping } from '../clipping-data';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 import { NullLocation } from '../../../mol-model/location';
 import { QuadPositions } from '../../../mol-gl/compute/util';
 import { QuadPositions } from '../../../mol-gl/compute/util';
+import { createEmptySubstance } from '../substance-data';
 
 
 const QuadIndices = new Uint32Array([
 const QuadIndices = new Uint32Array([
     0, 1, 2,
     0, 1, 2,
@@ -113,7 +114,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 = {
@@ -142,6 +146,7 @@ namespace Image {
         const marker = createMarkers(instanceCount * groupCount);
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
         const clipping = createEmptyClipping();
 
 
         const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
         const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
@@ -154,6 +159,7 @@ namespace Image {
             ...marker,
             ...marker,
             ...overpaint,
             ...overpaint,
             ...transparency,
             ...transparency,
+            ...material,
             ...clipping,
             ...clipping,
             ...transform,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
             ...BaseGeometry.createValues(props, counts),

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

@@ -6,7 +6,7 @@
 
 
 import { ValueCell } from '../../../mol-util';
 import { ValueCell } from '../../../mol-util';
 import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
-import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
+import { transformPositionArray, GroupMapping, createGroupMapping } from '../../util';
 import { GeometryUtils } from '../geometry';
 import { GeometryUtils } from '../geometry';
 import { createColors } from '../color-data';
 import { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
 import { createMarkers } from '../marker-data';
@@ -26,6 +26,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
 import { createEmptyClipping } from '../clipping-data';
+import { createEmptySubstance } from '../substance-data';
 
 
 /** Wide line */
 /** Wide line */
 export interface Lines {
 export interface Lines {
@@ -164,7 +165,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
@@ -210,6 +211,7 @@ export namespace Lines {
         const marker = createMarkers(instanceCount * groupCount);
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
         const clipping = createEmptyClipping();
 
 
         const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
         const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
@@ -231,6 +233,7 @@ export namespace Lines {
             ...marker,
             ...marker,
             ...overpaint,
             ...overpaint,
             ...transparency,
             ...transparency,
+            ...material,
             ...clipping,
             ...clipping,
             ...transform,
             ...transform,
 
 

+ 64 - 1
src/mol-geo/geometry/marker-data.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>
  */
  */
@@ -9,20 +9,75 @@ import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 
 
 export type MarkerData = {
 export type MarkerData = {
+    uMarker: ValueCell<number>,
     tMarker: ValueCell<TextureImage<Uint8Array>>
     tMarker: ValueCell<TextureImage<Uint8Array>>
     uMarkerTexDim: ValueCell<Vec2>
     uMarkerTexDim: ValueCell<Vec2>
+    dMarkerType: ValueCell<string>,
+    markerAverage: ValueCell<number>
+    markerStatus: ValueCell<number>
+}
+
+const MarkerCountLut = new Uint8Array(0x0303 + 1);
+MarkerCountLut[0x0001] = 1;
+MarkerCountLut[0x0002] = 1;
+MarkerCountLut[0x0003] = 1;
+MarkerCountLut[0x0100] = 1;
+MarkerCountLut[0x0200] = 1;
+MarkerCountLut[0x0300] = 1;
+MarkerCountLut[0x0101] = 2;
+MarkerCountLut[0x0201] = 2;
+MarkerCountLut[0x0301] = 2;
+MarkerCountLut[0x0102] = 2;
+MarkerCountLut[0x0202] = 2;
+MarkerCountLut[0x0302] = 2;
+MarkerCountLut[0x0103] = 2;
+MarkerCountLut[0x0203] = 2;
+MarkerCountLut[0x0303] = 2;
+
+/**
+ * Calculates the average number of entries that have any marker flag set.
+ *
+ * For alternative implementations and performance tests see
+ * `src\perf-tests\markers-average.ts`.
+ */
+export function getMarkersAverage(array: Uint8Array, count: number): number {
+    if (count === 0) return 0;
+
+    const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2);
+    const viewEnd = (count - 4) >> 2;
+    const backStart = 4 * viewEnd;
+
+    let sum = 0;
+    for (let i = 0; i < viewEnd; ++i) {
+        const v = view[i];
+        sum += MarkerCountLut[v & 0xFFFF] + MarkerCountLut[v >> 16];
+    }
+    for (let i = backStart; i < count; ++i) {
+        sum += array[i] && 1;
+    }
+    return sum / count;
 }
 }
 
 
 export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
 export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
     const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array);
     const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array);
+    const average = getMarkersAverage(markers.array, count);
+    const status = average === 0 ? 0 : -1;
     if (markerData) {
     if (markerData) {
+        ValueCell.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, markers);
         ValueCell.update(markerData.tMarker, markers);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height));
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height));
+        ValueCell.updateIfChanged(markerData.dMarkerType, status === -1 ? 'groupInstance' : 'uniform');
+        ValueCell.updateIfChanged(markerData.markerAverage, average);
+        ValueCell.updateIfChanged(markerData.markerStatus, status);
         return markerData;
         return markerData;
     } else {
     } else {
         return {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(markers),
             tMarker: ValueCell.create(markers),
             uMarkerTexDim: ValueCell.create(Vec2.create(markers.width, markers.height)),
             uMarkerTexDim: ValueCell.create(Vec2.create(markers.width, markers.height)),
+            markerAverage: ValueCell.create(average),
+            markerStatus: ValueCell.create(status),
+            dMarkerType: ValueCell.create('uniform'),
         };
         };
     }
     }
 }
 }
@@ -30,13 +85,21 @@ export function createMarkers(count: number, markerData?: MarkerData): MarkerDat
 const emptyMarkerTexture = { array: new Uint8Array(1), width: 1, height: 1 };
 const emptyMarkerTexture = { array: new Uint8Array(1), width: 1, height: 1 };
 export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
 export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
     if (markerData) {
     if (markerData) {
+        ValueCell.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, emptyMarkerTexture);
         ValueCell.update(markerData.tMarker, emptyMarkerTexture);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(1, 1));
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(1, 1));
+        ValueCell.updateIfChanged(markerData.dMarkerType, 'uniform');
+        ValueCell.updateIfChanged(markerData.markerAverage, 0);
+        ValueCell.updateIfChanged(markerData.markerStatus, 0);
         return markerData;
         return markerData;
     } else {
     } else {
         return {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(emptyMarkerTexture),
             tMarker: ValueCell.create(emptyMarkerTexture),
             uMarkerTexDim: ValueCell.create(Vec2.create(1, 1)),
             uMarkerTexDim: ValueCell.create(Vec2.create(1, 1)),
+            markerAverage: ValueCell.create(0),
+            markerStatus: ValueCell.create(0),
+            dMarkerType: ValueCell.create('uniform'),
         };
         };
     }
     }
 }
 }

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

@@ -21,7 +21,6 @@ const tmpCylinderCenter = Vec3();
 const tmpCylinderMat = Mat4();
 const tmpCylinderMat = Mat4();
 const tmpCylinderMatRot = Mat4();
 const tmpCylinderMatRot = Mat4();
 const tmpCylinderScale = Vec3();
 const tmpCylinderScale = Vec3();
-const tmpCylinderMatScale = Mat4();
 const tmpCylinderStart = Vec3();
 const tmpCylinderStart = Vec3();
 const tmpUp = Vec3();
 const tmpUp = Vec3();
 
 
@@ -32,9 +31,9 @@ function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number, matchDi
     // direction so the triangles of adjacent cylinder will line up
     // direction so the triangles of adjacent cylinder will line up
     if (matchDir) Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
     if (matchDir) Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
     else Vec3.copy(tmpUp, up);
     else Vec3.copy(tmpUp, up);
-    Mat4.fromScaling(tmpCylinderMatScale, Vec3.set(tmpCylinderScale, 1, length, 1));
+    Vec3.set(tmpCylinderScale, 1, length, 1);
     Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
     Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
-    Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);
+    Mat4.scale(m, tmpCylinderMatRot, tmpCylinderScale);
     return Mat4.setTranslation(m, tmpCylinderCenter);
     return Mat4.setTranslation(m, tmpCylinderCenter);
 }
 }
 
 

+ 1 - 1
src/mol-geo/geometry/mesh/builder/ribbon.ts

@@ -36,7 +36,7 @@ const torsionVector = Vec3();
 export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, arrowHeight: number) {
 export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, arrowHeight: number) {
     const { currentGroup, vertices, normals, indices, groups } = state;
     const { currentGroup, vertices, normals, indices, groups } = state;
 
 
-    let vertexCount = vertices.elementCount;
+    const vertexCount = vertices.elementCount;
     let offsetLength = 0;
     let offsetLength = 0;
 
 
     if (arrowHeight > 0) {
     if (arrowHeight > 0) {

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