Explorar el Código

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

Alexander Rose hace 3 años
padre
commit
76ed2e9e11
Se han modificado 100 ficheros con 5403 adiciones y 3836 borrados
  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",
                 "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": [
         {
@@ -89,7 +106,14 @@
                     "error",
                     "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]
 
-- 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``
 - 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
+- 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
 
@@ -77,29 +219,22 @@ Note that since we don't clearly distinguish between a public and private interf
 - Fixed Measurements UI labels (#166)
 
 ## [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] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension.
 
 ## [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.
-
-### Fixed
-- VolumeFromDensityServerCif transform label
-
+- Fix VolumeFromDensityServerCif transform label
 
 ## [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
+
 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.
 
+### 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:
     NODE_ENV=production npm run build
 
@@ -122,9 +133,9 @@ and navigate to `build/viewer`
 
 **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
 

+ 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_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_common
@@ -24,8 +24,8 @@ atom_type_scat.dispersion_imag
 atom_type_scat.source
 
 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
 
 cell.length_a
@@ -35,14 +35,14 @@ cell.angle_alpha
 cell.angle_beta
 cell.angle_gamma
 cell.volume
-cell.formula_units_Z
+cell.formula_units_z
 
 atom_site.label
 atom_site.type_symbol
 atom_site.fract_x
 atom_site.fract_y
 atom_site.fract_z
-atom_site.U_iso_or_equiv
+atom_site.u_iso_or_equiv
 atom_site.adp_type
 atom_site.occupancy
 atom_site.calc_flag
@@ -52,20 +52,13 @@ atom_site.disorder_group
 atom_site.site_symmetry_multiplicity
 
 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_2

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 462 - 280
package-lock.json


+ 55 - 50
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "2.2.0",
+  "version": "3.0.0-dev.3",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -16,6 +16,8 @@
     "test": "npm run lint && jest",
     "jest": "jest",
     "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-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
     "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",
     "plugin-state": "node lib/commonjs/servers/plugin-state/index.js --working-folder ./build/state --port 1339",
     "preversion": "npm run test",
-    "version": "npm run build",
+    "version": "npm run rebuild && cpx .npmignore lib/",
     "postversion": "git push && git push --tags"
   },
   "files": [
@@ -88,68 +90,71 @@
   ],
   "license": "MIT",
   "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",
-    "concurrently": "^5.3.0",
-    "cpx2": "^3.0.0",
+    "concurrently": "^6.4.0",
+    "cpx2": "^4.0.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",
     "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",
     "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",
-    "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": {
-    "@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",
     "compression": "^1.7.4",
     "cors": "^2.8.5",
     "express": "^4.17.1",
     "h264-mp4-encoder": "^1.0.12",
-    "immer": "^8.0.1",
+    "immer": "^9.0.7",
     "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 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) {
     if (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() {
     console.log('\n###', 'copy viewer files');
     const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
     const viewerDeployPath = path.resolve(localPath, 'viewer/');
     fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
+    addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
 }
 
 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 { StateObjectRef } from '../../mol-state';
 import { Color } from '../../mol-util/color';
+import { Material } from '../../mol-util/material';
 
 function shinyStyle(plugin: PluginContext) {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
         renderer: {
             ...plugin.canvas3d!.props.renderer,
-            style: { name: 'plastic', params: {} },
         },
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
@@ -40,7 +40,6 @@ function occlusionStyle(plugin: PluginContext) {
     return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
         renderer: {
             ...plugin.canvas3d!.props.renderer,
-            style: { name: 'flat', params: {} }
         },
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
@@ -52,7 +51,8 @@ function occlusionStyle(plugin: PluginContext) {
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,
-                threshold: 0.33
+                threshold: 0.33,
+                color: Color(0x0000),
             } }
         }
     } });
@@ -77,7 +77,7 @@ const PresetParams = {
     ...StructureRepresentationPresetProvider.CommonParams,
 };
 
-
+const CustomMaterial = Material({ roughness: 0.2, metalness: 0 });
 
 export const StructurePreset = StructureRepresentationPresetProvider({
     id: 'preset-structure',
@@ -94,8 +94,8 @@ export const StructurePreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         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 });
@@ -121,8 +121,8 @@ export const IllustrativePreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         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 });
@@ -149,8 +149,8 @@ const SurfacePreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         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 });
@@ -177,8 +177,8 @@ const PocketPreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         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 });
@@ -206,10 +206,10 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         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 });
@@ -238,7 +238,7 @@ export class ViewportComponent extends PluginUIComponent {
     pocketPreset = () => this.set(PocketPreset);
     interactionsPreset = () => this.set(InteractionsPreset);
 
-    get showButtons () {
+    get showButtons() {
         return this.plugin.config.get(ShowButtons);
     }
 

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

@@ -36,7 +36,7 @@
                 emdbProvider: 'rcsb',
             });
             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' } } })
         </script>

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

@@ -52,12 +52,22 @@
             var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1';
             var pdbProvider = getParam('pdb-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', {
                 layoutShowControls: !hideControls,
                 viewportShowExpand: false,
                 collapseLeftPanel: collapseLeftPanel,
                 pdbProvider: pdbProvider || '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();
@@ -81,5 +91,6 @@
             var emdb = getParam('emdb', '[^&]+').trim();
             if (emdb) viewer.loadEmdb(emdb);
         </script>
+        <!-- __MOLSTAR_ANALYTICS__ -->
     </body>
 </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 Alexander Rose <alexander.rose@weirdbyte.de>
@@ -9,25 +9,28 @@ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
 import { CellPack } from '../../extensions/cellpack';
 import { DnatcoConfalPyramids } from '../../extensions/dnatco';
 import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
-import { Mp4Export } from '../../extensions/mp4-export';
 import { GeometryExport } from '../../extensions/geo-export';
+import { Mp4Export } from '../../extensions/mp4-export';
 import { PDBeStructureQualityReport } from '../../extensions/pdbe';
 import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
 import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
 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 { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
+import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure';
 import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
 import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
 import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
 import { createPlugin } from '../../mol-plugin-ui';
 import { PluginUIContext } from '../../mol-plugin-ui/context';
-import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginConfig } from '../../mol-plugin/config';
+import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
 import { PluginSpec } from '../../mol-plugin/spec';
 import { PluginState } from '../../mol-plugin/state';
 import { StateObjectSelector } from '../../mol-state';
@@ -71,9 +74,12 @@ const DefaultViewerOptions = {
     layoutShowLog: true,
     layoutShowLeftPanel: true,
     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,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -112,7 +118,7 @@ export class Viewer {
                     regionState: {
                         bottom: 'full',
                         left: o.collapseLeftPanel ? 'collapsed' : 'full',
-                        right: 'full',
+                        right: o.collapseRightPanel ? 'hidden' : 'full',
                         top: 'full',
                     }
                 },
@@ -130,6 +136,8 @@ export class Viewer {
             config: [
                 [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
                 [PluginConfig.General.PixelScale, o.pixelScale],
+                [PluginConfig.General.PickScale, o.pickScale],
+                [PluginConfig.General.PickPadding, o.pickPadding],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [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() {
-        this.plugin.layout.events.updated.next();
+        this.plugin.layout.events.updated.next(void 0);
     }
 }
 
@@ -339,4 +397,16 @@ export interface VolumeIsovalueInfo {
     color: Color,
     alpha?: 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>) {
     const ionNames: string[] = [];
     for (const k in ccd) {
-        const {chem_comp} = ccd[k];
+        const { chem_comp } = ccd[k];
         if (chem_comp.name.value(0).toUpperCase().includes(' ION')) {
             ionNames.push(chem_comp.id.value(0));
         }
@@ -54,20 +54,20 @@ async function run(out: string, forceDownload = false) {
 }
 
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Extract and save IonNames from CCD.'
 });
-parser.addArgument('out', {
+parser.add_argument('out', {
     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.'
 });
 interface Args {
     out: string,
     forceDownload?: boolean,
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 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
     });
 
-    const bondDatabase =  Database.ofTables(
+    const bondDatabase = Database.ofTables(
         CCB_TABLE_NAME,
         { chem_comp_bond: mmCIF_chemCompBond_schema },
         { chem_comp_bond: bondTable }
@@ -265,21 +265,21 @@ const CCB_TABLE_NAME = 'CHEM_COMP_BONDS';
 const CCA_TABLE_NAME = 'CHEM_COMP_ATOMS';
 
 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.'
 });
-parser.addArgument('out', {
+parser.add_argument('out', {
     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.'
 });
-parser.addArgument([ '--binary', '-b' ], {
-    action: 'storeTrue',
+parser.add_argument('--binary', '-b', {
+    action: 'store_true',
     help: 'Output as BinaryCIF.'
 });
-parser.addArgument(['--ccaOut', '-a'], {
+parser.add_argument('--ccaOut', '-a', {
     help: 'Optional generated file output path for chem_comp_atom data.',
     required: false
 });
@@ -289,6 +289,6 @@ interface Args {
     binary?: boolean,
     ccaOut?: string
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 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({
-    addHelp: true,
+    add_help: true,
     description: 'Convert any CIF file to a BCIF file'
 });
-parser.addArgument([ 'src' ], {
+parser.add_argument('src', {
     help: 'Source CIF path'
 });
-parser.addArgument([ 'out' ], {
+parser.add_argument('out', {
     help: 'Output BCIF path'
 });
-parser.addArgument([ '-c', '--config' ], {
+parser.add_argument('-c', '--config', {
     help: 'Optional encoding strategy/precision config path',
     required: false
 });
-parser.addArgument([ '-f', '--filter' ], {
+parser.add_argument('-f', '--filter', {
     help: 'Optional filter whitelist/blacklist path',
     required: false
 });
@@ -61,7 +61,7 @@ interface Args {
     config?: string
     filter?: string
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 if (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 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 filter: Filter = {};
     fieldNames.forEach((name, i) => {
-        const [ category, field ] = name.split('.');
+        const [category, field] = name.split('.');
         // console.log(category, field)
-        if (!filter[ category ]) filter[ category ] = {};
-        filter[ category ][ field ] = true;
+        if (!filter[category]) filter[category] = {};
+        filter[category][field] = true;
     });
     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 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)'
 });
-parser.addArgument([ '--preset', '-p' ], {
-    defaultValue: '',
+parser.add_argument('--preset', '-p', {
+    default: '',
     choices: ['', 'mmCIF', 'CCD', 'BIRD', 'CifCore'],
     help: 'Preset name'
 });
-parser.addArgument([ '--name', '-n' ], {
-    defaultValue: '',
+parser.add_argument('--name', '-n', {
+    default: '',
     help: 'Schema name'
 });
-parser.addArgument([ '--out', '-o' ], {
+parser.add_argument('--out', '-o', {
     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'],
     help: 'Target format'
 });
-parser.addArgument([ '--dicPath', '-d' ], {
-    defaultValue: '',
+parser.add_argument('--dicPath', '-d', {
+    default: '',
     help: 'Path to dictionary'
 });
-parser.addArgument([ '--fieldNamesPath', '-fn' ], {
-    defaultValue: '',
+parser.add_argument('--fieldNamesPath', '-fn', {
+    default: '',
     help: 'Field names to include'
 });
-parser.addArgument([ '--forceDicDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDicDownload', '-f', {
+    action: 'store_true',
     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)'
 });
-parser.addArgument([ '--addAliases', '-aa' ], {
-    action: 'storeTrue',
+parser.add_argument('--addAliases', '-aa', {
+    action: 'store_true',
     help: 'Add field name/path aliases'
 });
 interface Args {
@@ -230,7 +230,7 @@ interface Args {
     moldataImportPath: string
     addAliases: boolean
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 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 'author':
         case 'orcid_id':
+        case 'pdbx_PDB_obsoleted_db_id':
+        case 'pdbx_related_db_id':
         case 'sequence_dep':
         case 'pdb_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,Real,Real)':
         case 'Date':
-        case 'Datetime':
+        case 'DateTime':
         case 'Tag':
         case 'Implied':
+        case 'Word':
             return wrapContainer('str', ',', description, container);
         case 'Real':
             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 {
     const code = getField('item_type', 'code', d, imports, ctx) || getField('type', 'contents', d, imports, ctx);
     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 {
         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',
 ];
 
+/**
+ * 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 } = {
-    '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 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 => {
         // category definitions in mmCIF start with '_' and 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();
         // console.log(d.header, d.categoryNames, d.categories)
         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');
             if (child_name && parent_name) {
                 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);
                     if (childName in links && links[childName] !== 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 { FieldPath } from '../../../mol-io/reader/cif/schema';
 
-function header (name: string, info: string, moldataImportPath: string) {
+function header(name: string, info: string, moldataImportPath: string) {
     return `/**
  * 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;`;
 }
 
-function footer (name: string) {
+function footer(name: string) {
     return `
 export type ${name}_Schema = typeof ${name}_Schema;
 export interface ${name}_Database extends Database<${name}_Schema> {};`;
@@ -89,7 +89,7 @@ function doc(description: string, spacesCount: number) {
     ].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[] = [];
 
     if (fields) {
@@ -128,7 +128,7 @@ export function generate (name: string, info: string, schema: Database, fields:
         codeLines.push('');
         codeLines.push(`export const ${name}_Aliases = {`);
         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][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}]
     // [{"file":'templ_enum.cif' "save":'H_M_ref'}]
     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 {
             save: save ? save[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 function mergeFilters (...filters: Filter[]) {
+export function mergeFilters(...filters: Filter[]) {
     const n = filters.length;
     const mergedFilter: Filter = {};
     const fields: Map<string, number> = new Map();
     filters.forEach(filter => {
         Object.keys(filter).forEach(category => {
-            Object.keys(filter[ category ]).forEach(field => {
+            Object.keys(filter[category]).forEach(field => {
                 const key = `${category}.${field}`;
                 const value = fields.get(key) || 0;
                 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({
-    addHelp: true,
+    add_help: true,
     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'
 });
-parser.addArgument([ '--forceDownload', '-f' ], {
-    action: 'storeTrue',
+parser.add_argument('--forceDownload', '-f', {
+    action: 'store_true',
     help: 'Force download of martini lipids itp'
 });
 interface Args {
     out: string
     forceDownload: boolean
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 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;
     let rI = 0;
     while (rI < count) {
-        let start = rI;
+        const start = rI;
         while (rI < count && key[start] === key[rI]) rI++;
         rI--;
 
@@ -230,21 +230,21 @@ async function runFile(filename: string, args: Args) {
 }
 
 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'
 });
-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 {
     download?: string,
     file?: string,
@@ -260,7 +260,7 @@ interface Args {
     mod?: boolean,
     sec?: boolean,
 }
-const args: Args = parser.parseArgs();
+const args: Args = parser.parse_args();
 
 if (args.download) runDL(args.download, 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) {
-    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 });
 
     // Export the mesh in OBJ format.
@@ -75,13 +75,13 @@ async function run(url: string, meshFilename: string) {
 }
 
 const parser = new argparse.ArgumentParser({
-    addHelp: true,
+    add_help: true,
     description: 'Info about VolumeData from mol-model module'
 });
-parser.addArgument([ '--emdb', '-e' ], {
+parser.add_argument('--emdb', '-e', {
     help: 'EMDB id, for example 8116',
 });
-parser.addArgument([ '--mesh' ], {
+parser.add_argument('--mesh', {
     help: 'Mesh filename',
     required: true
 });
@@ -89,6 +89,6 @@ interface Args {
     emdb?: 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);

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

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

+ 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 { PluginCommands } from '../../mol-plugin/commands';
 import { Asset } from '../../mol-util/assets';
+import { Color } from '../../mol-util/color';
 import './index.html';
 require('mol-plugin-ui/skin/light.scss');
 
@@ -26,7 +27,7 @@ const Canvas3DPresets = {
         },
         postprocessing: {
             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: {
             style: { name: 'flat', params: {} }
@@ -105,7 +106,7 @@ class LightingDemo {
                 ...this.plugin.canvas3d!.props.postprocessing,
                 ...props.postprocessing
             },
-        }});
+        } });
     }
 
     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) {
         if (!this.plugin.canvas3d) return;
         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() {

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

@@ -39,22 +39,34 @@ interface ANVILContext {
 };
 
 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' }),
-    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' }),
-    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 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:
  * Membrane positioning for high- and low-resolution protein structures through a binary classification approach
  * Guillaume Postic, Yassine Ghouzam, Vincent Guiraud, and Jean-Christophe Gelly
  * Protein Engineering, Design & Selection, 2015, 1–5
  * 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) {
     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 exposed = new Array<number>();
     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();
     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];
             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;
             }
 
@@ -121,7 +138,7 @@ async function initialize(structure: Structure, props: ANVILProps, accessibleSur
             offsets.push(structure.serialMapping.getSerialIndex(l.unit, l.element));
             if (AccessibleSurfaceArea.getValue(l, accessibleSurfaceArea) / MaxAsa[label_comp_id(l)] > asaCutoff) {
                 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 center =  v3zero();
+    const center = v3zero();
     v3sub(normalVector, membrane.planePoint1, membrane.planePoint2);
     v3normalize(normalVector, normalVector);
 
@@ -344,7 +361,7 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
     // collect all residues in membrane layer
     for (let k = 0, kl = offsets.length; k < kl; 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]];
 
         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++) {
         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]];
 
         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;
                 endOffset = k;
-            } else  {
+            } else {
                 lastAuthSeqId++;
                 endOffset++;
             }
@@ -428,6 +445,10 @@ function membraneSegments(ctx: ANVILContext, membrane: MembraneCandidate): Array
     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 */
 function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid: Vec3): number {
     const { offsets, structure } = ctx;
@@ -455,11 +476,11 @@ function adjustExtent(ctx: ANVILContext, membrane: MembraneCandidate, centroid:
 }
 
 function qValue(currentStats: HphobHphil, initialStats: HphobHphil): number {
-    if(initialStats.hphob < 1) {
+    if (initialStats.hphob < 1) {
         initialStats.hphob = 0.1;
     }
 
-    if(initialStats.hphil < 1) {
+    if (initialStats.hphil < 1) {
         initialStats.hphil += 1;
     }
 
@@ -484,7 +505,7 @@ function generateSpherePoints(ctx: ANVILContext, numberOfSpherePoints: number):
     const { centroid, extent } = ctx;
     const points = [];
     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);
         theta = Math.acos(h);
         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). */

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

@@ -52,7 +52,7 @@ export const ANVILMembraneOrientation = PluginBehavior.create<{ autoAttach: bool
         }
 
         update(p: { autoAttach: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             return updated;
@@ -150,7 +150,7 @@ export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
     params: () => StructureRepresentationPresetProvider.CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
-        const structure  = structureCell?.obj?.data;
+        const structure = structureCell?.obj?.data;
         if (!structureCell || !structure) return {};
 
         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>
  */
@@ -33,7 +33,7 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
 
     if (ctx.structure && info) {
         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];
 
@@ -48,7 +48,7 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
                 hue, chroma: [30, 80], luminance: [15, 85],
                 clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
             }
-        }}, { minLabel: 'Min', maxLabel: 'Max' });
+        } }, { minLabel: 'Min', maxLabel: 'Max' });
         legend = palette.legend;
         const modelColor = new Map<number, Color>();
         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>
  */

+ 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>
  */
 
@@ -49,7 +49,7 @@ function ResampleControlPoints(points: NumberArray, segmentLength: number) {
     // controlPoints.Insert(0, controlPoints[0] + (controlPoints[0] - controlPoints[1]) / 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[1]);
 
@@ -111,7 +111,7 @@ function GetSmoothNormals(points: Vec3[]) {
     let p1 = points[1];
     let p2 = points[2];
     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);
     Vec3.normalize(prevV, p0121);
     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 tan_L_i = Vec3.sub(mfTmpV6, frames[i].t, v1t);
         // # 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);
         // compute r_(i+1) = R_2 * r_i^L
         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[] = [];
     if (resample) new_points = ResampleControlPoints(points, segmentLength);
     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));
         }
     }
@@ -211,7 +211,7 @@ export function getMatFromResamplePoints(points: NumberArray, segmentLength: num
         if (d >= segmentLength) {
             // 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 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));
             // let pos:Vec3 = Vec3.add(Vec3.zero(),pti1,pti)
             // 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 {
     name: string,
-    location: 'surface' | 'interior' | 'cytoplasme',
+    location: 'surface' | 'interior' | 'cytoplasme'
     ingredients: Packing['ingredients']
+    compartment?: CellCompartment
 }
 
-//
+export interface CellCompartment {
+    filename?: string
+    geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
+    compartment_primitives?: CompartmentPrimitives
+}
 
 export interface Cell {
     recipe: Recipe
+    options?: RecipeOptions
     cytoplasme?: Packing
     compartments?: { [key: string]: Compartment }
+    mapping_ids?: { [key: number]: [number, string] }
+}
+
+export interface RecipeOptions {
+    resultfile?: string
 }
 
 export interface Recipe {
@@ -35,8 +46,29 @@ export interface Recipe {
 export interface Compartment {
     surface?: 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 {
     ingredients: { [key: string]: Ingredient }
 }
@@ -64,18 +96,20 @@ export interface Ingredient {
     [curveX: string]: unknown;
     /** the orientation in the membrane */
     principalAxis?: Vec3;
+    principalVector?: Vec3;
     /** offset along membrane */
     offset?: Vec3;
     ingtype?: string;
     color?: Vec3;
     confidence?: number;
+    Type?: string;
 }
 
 export interface IngredientSource {
     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.. */
-    model?: string;     /** model number e.g 0,1,2... */
+    model?: string; /** model number e.g 0,1,2... */
     transform: {
         center: boolean;
         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>
  */

+ 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.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
 
 import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state';
 import { PluginContext } from '../../mol-plugin/context';
 import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
 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 { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
 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 { Task, RuntimeContext } from '../../mol-task';
 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 { getMatFromResamplePoints } from './curve';
 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 { Asset } from '../../mol-util/assets';
 import { Color } from '../../mol-util/color';
-import { readFromFile } from '../../mol-util/data-source';
 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) {
     return `${baseUrl}/results/${fileName}`;
@@ -41,12 +43,16 @@ class TrajectoryCache {
     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 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 assets: Asset.Wrapper[] = [];
+    const assets: Asset.Wrapper[] = [];
     if (!trajectory) {
         if (file) {
             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}'`);
             }
         } else if (id.match(/^[1-9][a-zA-Z0-9]{3,3}$/i)) {
-            if (surface){
+            if (surface) {
                 try {
                     const data = await getFromOPM(plugin, id, assetManager);
                     assets.push(data.asset);
+                    data.pdb.id! = id.toUpperCase();
                     trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
                 } catch (e) {
                     // fallback to getFromPdb
@@ -100,7 +107,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
     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);
     const { assembly } = props;
 
@@ -108,11 +115,12 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
         structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly));
     }
     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([
             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')])
             })
         ]);
@@ -123,11 +131,11 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
             })
         ]);
     }
-
     const compiled = compile<StructureSelection>(query);
     const result = compiled(new QueryContext(structure));
     structure = StructureSelection.unionStructure(result);
-
+    // change here if possible the label ?
+    // structure.label =  source.name;
     return structure;
 }
 
@@ -141,9 +149,9 @@ function getTransformLegacy(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 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);
     return m;
 }
@@ -157,9 +165,9 @@ function getCurveTransforms(ingredient: Ingredient) {
     const n = ingredient.nbCurve || 0;
     const instances: Mat4[] = [];
     let segmentLength = 3.4;
-    if (ingredient.uLength){
+    if (ingredient.uLength) {
         segmentLength = ingredient.uLength;
-    } else if (ingredient.radii){
+    } else if (ingredient.radii) {
         segmentLength = ingredient.radii[0].radii
             ? ingredient.radii[0].radii[0] * 2.0
             : 3.4;
@@ -168,7 +176,7 @@ function getCurveTransforms(ingredient: Ingredient) {
     for (let i = 0; i < n; ++i) {
         const cname = `curve${i}`;
         if (!(cname in ingredient)) {
-            // console.warn(`Expected '${cname}' in ingredient`)
+            console.warn(`Expected '${cname}' in ingredient`);
             continue;
         }
         const _points = ingredient[cname] as Vec3[];
@@ -177,9 +185,9 @@ function getCurveTransforms(ingredient: Ingredient) {
             continue;
         }
         // 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) {
-            console.info(distance);
+            // console.info(distance);
             resampling = true;
         }
         const points = new Float32Array(_points.length * 3);
@@ -190,13 +198,13 @@ function getCurveTransforms(ingredient: Ingredient) {
     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;
 
     for (let i = 0, il = transforms.length; i < il; ++i) {
         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) {
             builder.addWithOperator(unit, op);
         }
@@ -307,13 +315,13 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
     });
 
     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;
     if (source.pdb === 'None') return;
-
     const file = ingredientFiles[source.pdb];
     if (!file) {
         // 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
-    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;
-
     let structure: Structure;
     if (nbCurve) {
         structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
     } else {
+        if ((!results || results.length === 0)) return;
         let bu: string|undefined = source.bu ? source.bu : undefined;
-        if (bu){
+        if (bu) {
             if (bu === 'AU') {
                 bu = undefined;
             } else {
                 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
         let legacy: boolean = true;
-        if (ingredient.offset || ingredient.principalAxis){
+        const pcp = ingredient.principalVector ? ingredient.principalVector : ingredient.principalAxis;
+        if (pcp) {
             legacy = false;
             const structureMean = getStructureMean(structure);
             Vec3.negate(structureMean, structureMean);
             const m1: Mat4 = Mat4.identity();
             Mat4.setTranslation(m1, structureMean);
             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();
-                    Mat4.setTranslation(m, ingredient.offset);
+                    Mat4.setTranslation(m, o);
                     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();
-                    Quat.rotationTo(q, ingredient.principalAxis, Vec3.unitZ);
+                    Quat.rotationTo(q, p, Vec3.unitZ);
                     const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
                     structure = Structure.transform(structure, m);
                 }
             }
         }
-        structure = getAssembly(getResultTransforms(results, legacy), structure);
+
+        structure = getAssembly(name, getResultTransforms(results, legacy), structure);
     }
 
     return { structure, assets };
 }
 
+
 export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) {
     return Task.create('Create Packing Structure', async ctx => {
-        const { ingredients, name } = packing;
+        const { ingredients, location, name } = packing;
         const assets: Asset.Wrapper[] = [];
         const trajCache = new TrajectoryCache();
         const structures: Structure[] = [];
         const colors: Color[] = [];
-        let skipColors: boolean = false;
         for (const iName in ingredients) {
             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) {
                 structures.push(ingredientStructure.structure);
                 assets.push(...ingredientStructure.assets);
                 const c = ingredients[iName].color;
-                if (c){
+                if (c) {
                     colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
                 } 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) {
             if (ctx.shouldUpdate) await ctx.update(`${s.label}`);
             let maxInvariantId = 0;
-            let maxChainGroupId = 0;
+            const maxChainGroupId = 0;
             for (const u of s.units) {
                 const invariantId = u.invariantId + offsetInvariantId;
                 const chainGroupId = u.chainGroupId + offsetChainGroupId;
@@ -414,21 +429,20 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
         }
 
         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 });
         }
-        return { structure, assets, colors: skipColors ? undefined : colors };
+        return { structure, assets, colors: colors };
     });
 }
 
 async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) {
     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 json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false));
             const points = json.data.points as number[];
-
             const curve0: Vec3[] = [];
             for (let j = 0, jl = points.length; j < jl; j += 3) {
                 curve0.push(Vec3.fromArray(Vec3(), points, j));
@@ -454,7 +468,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
                 break;
             }
         }
-        if (!file){
+        if (!file) {
             // check for cif directly
             const cifileName = `${name}.cif`;
             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();
     if (file) {
         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 } });
         }
     } 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) {
     const ingredientFiles = params.ingredients || [];
 
     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') {
         const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl));
         cellPackJson = state.build().toRoot()
@@ -506,29 +576,36 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
             return;
         }
 
-        let jsonFile: Asset.File;
+        let modelFile: Asset.File;
         if (file.name.toLowerCase().endsWith('.zip')) {
             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) => {
                 if (k === 'model.json') return;
+                if (k === 'results.bin') return;
                 ingredientFiles.push(Asset.File(new File([v], k)));
             });
         } else {
-            jsonFile = file;
+            modelFile = file;
         }
-
         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
         .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 { packings } = cellPackObject.obj!.data;
 
+    const { packings } = cellPackObject.obj!.data;
     await handleHivRna(plugin, packings, params.baseUrl);
 
     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,
         };
         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', [
             ['blood_hiv_immature_inside.json', 'Blood 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'],
-            ['InfluenzaModel2.json', 'Influenza Complete'],
+            ['InfluenzaModel2.json', 'Influenza complete'],
             ['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.`' }),
-        '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']] }),
     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({
         traceOnly: PD.Boolean(false),
         representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
@@ -581,4 +680,4 @@ export const LoadCellPackModel = StateAction.build({
     from: PSO.Root
 })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
     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 Ludovic Autin <ludovic.autin@gmail.com>
  */
 
 import { StateObjectRef } from '../../mol-state';
@@ -9,8 +10,6 @@ import { StructureRepresentationPresetProvider, presetStaticComponent } from '..
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ColorNames } from '../../mol-util/color/names';
 import { CellPackGenerateColorThemeProvider } from './color/generate';
-import { CellPackInfoProvider } from './property';
-import { CellPackProvidedColorThemeProvider } from './color/provided';
 
 export const CellpackPackingPresetParams = {
     traceOnly: PD.Boolean(true),
@@ -42,8 +41,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
             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 representations = {
@@ -92,4 +91,4 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
 
         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>
  */
@@ -34,4 +34,4 @@ export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellP
             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 Ludovic Autin <ludovic.autin@gmail.com>
  */
 
 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 { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure';
 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 { ParseCellPack };
@@ -26,26 +31,173 @@ const ParseCellPack = PluginStateTransform.BuiltIn({
     name: 'parse-cellpack',
     display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' },
     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 => {
             const cell = a.data as Cell;
-
+            let counter_id = 0;
+            let fiber_counter_id = 0;
+            let comp_counter = 0;
             const packings: CellPacking[] = [];
             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) {
                 for (const name in compartments) {
                     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 });
         });
-    }
+    },
+    dispose({ cache }) {
+        ((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose();
+    },
 });
 
 export { StructureFromCellpack };
@@ -77,14 +229,13 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
             await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
                 info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
             });
-
             (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 }) {
         const assets = (cache as any).assets as Asset.Wrapper[];
-        if(assets) {
+        if (assets) {
             for (const a of assets) a.dispose();
         }
 
@@ -115,17 +266,17 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
             // TODO: optimze
             // TODO: think of ways how to fast-track changes to this for animations
             const model = a.data;
-            let initial_structure = Structure.ofModel(model);
+            const initial_structure = Structure.ofModel(model);
             const structures: Structure[] = [];
             let structure: Structure = initial_structure;
             // the list of asambly *?
             const symmetry = ModelSymmetry.Provider.get(model);
-            if (symmetry && symmetry.assemblies.length !== 0){
+            if (symmetry && symmetry.assemblies.length !== 0) {
                 for (const a of symmetry.assemblies) {
                     const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
                     structures.push(s);
                 }
-                const builder = Structure.Builder();
+                const builder = Structure.Builder({ label: 'Membrane' });
                 let offsetInvariantId = 0;
                 for (const s of structures) {
                     let maxInvariantId = 0;
@@ -137,7 +288,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
                     offsetInvariantId += maxInvariantId + 1;
                 }
                 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 });
                 }
             }
@@ -148,3 +299,28 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
         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 Ludovic Autin <ludovic.autin@gmail.com>
  */
 
 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) {
-    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 };
 }
 
-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'));
     return { pdb: await parsePDBfile(plugin, asset.data, pdbId), asset };
 }
@@ -74,4 +75,35 @@ export function getStructureMean(structure: Structure) {
     }
     const { elementCount } = structure;
     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;
         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 });
-        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)],
     ['BII', Color(0x0059DA)],
     ['miB', Color(0x3BE8FB)],
-    ['Z',  Color(0x01F60E)],
+    ['Z', Color(0x01F60E)],
     ['IC', Color(0xFA5CFB)],
     ['OPN', Color(0xE90000)],
     ['SYN', Color(0xFFFF01)],
@@ -165,8 +165,8 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
         legend: TableLegend(iterableToArray(ColorMapping.entries()).map(([conformer, color]) => {
             return [conformer, color] as [string, Color];
         }).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 second = residueInfoFromLocation(locSecond);
             const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
-            const alt_id_1 =  fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
+            const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
             const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
             const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
             const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';

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

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

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

@@ -4,7 +4,6 @@
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
  */
 
-import { getStyle } from '../../mol-gl/renderer';
 import { Box3D } from '../../mol-math/geometry';
 import { PluginComponent } from '../../mol-plugin-state/component';
 import { PluginContext } from '../../mol-plugin/context';
@@ -46,13 +45,12 @@ export class GeometryControls extends PluginComponent {
                 const renderObjects = this.plugin.canvas3d?.getRenderObjects()!;
                 const filename = this.getFilename();
 
-                const style = getStyle(this.plugin.canvas3d?.props.renderer.style!);
                 const boundingSphere = this.plugin.canvas3d?.boundingSphereVisible!;
                 const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
                 let renderObjectExporter: GlbExporter | ObjExporter | StlExporter | UsdzExporter;
                 switch (this.behaviors.params.value.format) {
                     case 'glb':
-                        renderObjectExporter = new GlbExporter(style, boundingBox);
+                        renderObjectExporter = new GlbExporter(boundingBox);
                         break;
                     case 'obj':
                         renderObjectExporter = new ObjExporter(filename, boundingBox);
@@ -61,7 +59,7 @@ export class GeometryControls extends PluginComponent {
                         renderObjectExporter = new StlExporter(boundingBox);
                         break;
                     case 'usdz':
-                        renderObjectExporter = new UsdzExporter(style, boundingBox, boundingSphere.radius);
+                        renderObjectExporter = new UsdzExporter(boundingBox, boundingSphere.radius);
                         break;
                     default: throw new Error('Unsupported format.');
                 }
@@ -77,7 +75,7 @@ export class GeometryControls extends PluginComponent {
                     filename: filename + '.' + renderObjectExporter.fileExtension
                 };
             } catch (e) {
-                this.plugin.log.error('' + e);
+                this.plugin.log.error('Error during geometry export');
                 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.
  *
  * @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 { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
 import { Box3D } from '../../mol-math/geometry';
@@ -15,7 +14,7 @@ import { RuntimeContext } from '../../mol-task';
 import { Color } from '../../mol-util/color/color';
 import { fillSerial } from '../../mol-util/array';
 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)
 const v3fromArray = Vec3.fromArray;
@@ -30,6 +29,12 @@ const FLOAT = 5126;
 const ARRAY_BUFFER = 34962;
 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 = {
     glb: Uint8Array
 }
@@ -38,6 +43,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
     readonly fileExtension = 'glb';
     private nodes: 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 bufferViews: Record<string, any>[] = [];
     private binaryBuffer: ArrayBuffer[] = [];
@@ -53,7 +60,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
                 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) {
@@ -108,7 +115,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
             indexArray = indices!.slice(0, drawCount);
         }
 
-        const [ vertexMin, vertexMax ] = GlbExporter.vec3MinMax(vertexArray);
+        const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
 
         let vertexBuffer = vertexArray.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 dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
 
         const colorArray = new Uint8Array(vertexCount * 4);
 
         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.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);
     }
 
+    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) {
         const { mesh, values, isGeoTexture, webgl, ctx } = input;
 
         const t = Mat4();
 
         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 aTransform = values.aTransform.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') {
             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
@@ -235,7 +235,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
 
                 // create a color buffer if needed
                 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
@@ -248,7 +248,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
                             COLOR_0: colorAccessorIndex!
                         },
                         indices: indexAccessorIndex,
-                        material: 0
+                        material
                     }]
                 });
             }
@@ -282,13 +282,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
             }],
             bufferViews: this.bufferViews,
             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] => {
@@ -307,19 +301,19 @@ export class GlbExporter extends MeshExporter<GlbData> {
             if (padding) {
                 chunk.push(padding.buffer);
             }
-            return [ chunk, 8 + byteLength ];
+            return [chunk, 8 + byteLength];
         };
         const jsonString = JSON.stringify(gltf);
         const jsonBuffer = new Uint8Array(jsonString.length);
         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 header = new ArrayBuffer(12);
         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(8, glbBufferLength, true); // length
         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' });
     }
 
-    constructor(private style: Style, boundingBox: Box3D) {
+    constructor(boundingBox: Box3D) {
         super();
         const tmpV = Vec3();
         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.
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { sort, arraySwap } from '../../mol-data/util';
@@ -26,6 +27,7 @@ import { RuntimeContext } from '../../mol-task';
 import { Color } from '../../mol-util/color/color';
 import { decodeFloatRGB } from '../../mol-util/float-packing';
 import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
+import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
 
 const GeoExportName = 'geo-export';
 
@@ -48,6 +50,14 @@ export interface AddMeshInput {
     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> {
     abstract readonly fileExtension: string;
 
@@ -90,26 +100,43 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
         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 colorGridDim = values.uColorGridDim.ref.value;
         const colorTexDim = values.uColorTexDim.ref.value;
         const aTransform = values.aTransform.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;
     }
 
@@ -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;
 
     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 [ width, height ] = values.uGeoTexDim.ref.value;
+        const [width, height] = values.uGeoTexDim.ref.value;
         const vertices = 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);

+ 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.
  *
  * @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { asciiWrite } from '../../mol-io/common/ascii';
@@ -47,7 +48,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
         StringBuilder.newline(this.obj);
         if (!this.materialSet.has(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;
             StringBuilder.writeSafe(mtl, `newmtl ${material}\n`);
             StringBuilder.writeSafe(mtl, 'illum 2\n'); // illumination model
@@ -77,19 +78,27 @@ export class ObjExporter extends MeshExporter<ObjData> {
         const tmpV = Vec3();
         const stride = isGeoTexture ? 4 : 3;
 
-        const groupCount = values.uGroupCount.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 dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.ref.value;
         const instanceCount = values.uInstanceCount.ref.value;
 
-        let interpolatedColors: Uint8Array;
+        let interpolatedColors: Uint8Array | undefined;
         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 });
@@ -127,47 +136,23 @@ export class ObjExporter extends MeshExporter<ObjData> {
                 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
             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);
 

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

@@ -79,10 +79,10 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         try {
             this.setState({ busy: true });
             const data = await this.controls.exportGeometry();
-            this.setState({ busy: false });
-
             download(data.blob, data.filename);
-        } catch {
+        } catch (e) {
+            console.error(e);
+        } finally {
             this.setState({ busy: false });
         }
     }
@@ -91,7 +91,6 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         try {
             this.setState({ busy: true });
             const data = await this.controls.exportGeometry();
-            this.setState({ busy: false });
             const a = document.createElement('a');
             a.rel = 'ar';
             a.href = URL.createObjectURL(data.blob);
@@ -100,7 +99,9 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
             a.appendChild(document.createElement('img'));
             setTimeout(() => URL.revokeObjectURL(a.href), 4E4); // 40s
             setTimeout(() => a.dispatchEvent(new MouseEvent('click')));
-        } catch {
+        } catch (e) {
+            console.error(e);
+        } finally {
             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.
  *
  * @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 { Box3D } from '../../mol-math/geometry';
 import { Vec3, Mat3, Mat4 } from '../../mol-math/linear-algebra';
@@ -31,18 +31,17 @@ export class UsdzExporter extends MeshExporter<UsdzData> {
     readonly fileExtension = 'usdz';
     private meshes: string[] = [];
     private materials: string[] = [];
-    private materialSet = new Set<number>();
+    private materialMap = new Map<string, number>();
     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(`
 def Material "material${materialKey}"
 {
@@ -52,12 +51,13 @@ def Material "material${materialKey}"
         uniform token info:id = "UsdPreviewSurface"
         color3f inputs:diffuseColor = (${r},${g},${b})
         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
     }
 }
 `);
+        return materialKey;
     }
 
     protected async addMeshWithColors(input: AddMeshInput) {
@@ -68,19 +68,30 @@ def Material "material${materialKey}"
         const tmpV = Vec3();
         const stride = isGeoTexture ? 4 : 3;
 
-        const groupCount = values.uGroupCount.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 dTransparency = values.dTransparency.ref.value;
-        const tTransparency = values.tTransparency.ref.value;
         const aTransform = values.aTransform.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') {
-            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 });
@@ -122,6 +133,8 @@ def Material "material${materialKey}"
                 StringBuilder.writeSafe(normalBuilder, ')');
             }
 
+            const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
+
             // face
             for (let i = 0; i < drawCount; ++i) {
                 const v = isGeoTexture ? i : indices![i];
@@ -130,51 +143,23 @@ def Material "material${materialKey}"
             }
 
             // color
-            const faceIndicesByMaterial = new Map<number, number[]>();
+            const quantizedColors = new Uint8Array(drawCount * 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);
                 if (faceIndices === undefined) {
                     faceIndices = [];
@@ -246,7 +231,7 @@ def Mesh "mesh${this.meshes.length}"
         return new Blob([usdz], { type: 'model/vnd.usdz+zip' });
     }
 
-    constructor(private style: Style, boundingBox: Box3D, radius: number) {
+    constructor(boundingBox: Box3D, radius: number) {
         super();
         const t = Mat4();
         // 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, '');
                 return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
             } catch (e) {
-                this.plugin.log.error('' + e);
+                this.plugin.log.error('Error during animation export');
                 throw e;
             }
         });

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

@@ -115,7 +115,8 @@ export class Mp4EncoderUI extends CollapsableControls<{}, State> {
             this.setState({ busy: true });
             const data = await this.controls.render();
             this.setState({ busy: false, data });
-        } catch {
+        } catch (e) {
+            console.error(e);
             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;
 
         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);
             if (!data) return false;
             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 }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.showTooltip = p.showTooltip;
             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',
     label: 'Structure Quality Report',
     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 {
-        let info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
+        const info = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
         if (!info) return;
         const data = getCifData(model);
         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 }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
             return updated;
@@ -85,7 +85,7 @@ export const InitAssemblySymmetry3D = StateAction.build({
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
         await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
-    } catch(e) {
+    } catch (e) {
         plugin.log.error(`Assembly Symmetry: ${e}`);
         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) {
                 const { symbol, kind } = assemblySymmetryData[i];
                 if (symbol !== 'C1') {
-                    options.push([ i, `${i + 1}: ${symbol} ${kind}` ]);
+                    options.push([i, `${i + 1}: ${symbol} ${kind}`]);
                 }
             }
             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 circle = polygon(12, false, 1);
     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 { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
 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 { ParamDefinition as PD } from '../../../mol-util/param-definition';
 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
 documents: './src/extensions/rcsb/graphql/symmetry.gql.ts'
 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

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 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 }) {
-            let updated = this.params.autoAttach !== p.autoAttach;
+            const updated = this.params.autoAttach !== p.autoAttach;
             this.params.autoAttach = p.autoAttach;
             this.params.showTooltip = p.showTooltip;
             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>> {
-        switch(props.source.name) {
+        switch (props.source.name) {
             case 'file': return open(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) {
             // 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) {
                 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) {
         // 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) {
             unit.conformation.position(a[i], pA);
@@ -431,7 +431,7 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
 
     const groups = xml.getElementsByTagName('ModelledSubgroup');
     for (let i = 0, il = groups.length; i < il; ++i) {
-        const g = groups[ i ];
+        const g = groups[i];
         const ga = g.attributes;
 
         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[]) {
     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));
 }
 
@@ -125,7 +125,7 @@ function getIntraClashLoci(pickingId: PickingId, structureGroup: StructureGroup,
 }
 
 function eachIntraClash(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
-    let changed = false;
+    const changed = false;
     // TODO
     return changed;
 }
@@ -226,7 +226,7 @@ function getInterClashLabel(structure: Structure, clashes: InterUnitClashes, ele
 
 function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
     return DataLoci('inter-clashes', clashes, elements,
-        (boundingSphere: Sphere3D) =>  getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
+        (boundingSphere: Sphere3D) => getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
         () => 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) {
-    let changed = false;
+    const changed = false;
     // TODO
     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 Alexander Rose <alexander.rose@weirdbyte.de>
@@ -27,6 +27,10 @@ interface ICamera {
     readonly fogNear: number,
 }
 
+const tmpPos1 = Vec3();
+const tmpPos2 = Vec3();
+const tmpClip = Vec4();
+
 class Camera implements ICamera {
     readonly view: Mat4 = Mat4.identity();
     readonly projection: Mat4 = Mat4.identity();
@@ -34,7 +38,7 @@ class Camera implements ICamera {
     readonly inverseProjectionView: Mat4 = Mat4.identity();
 
     private pixelScale: number
-    get pixelRatio () {
+    get pixelRatio() {
         const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
         return dpr * this.pixelScale;
     }
@@ -155,14 +159,32 @@ class Camera implements ICamera {
         }
     }
 
+    /** Transform point into 2D window coordinates. */
     project(out: Vec4, point: Vec3) {
         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);
     }
 
+    /** 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 }> = {}) {
         this.viewport = viewport;
         this.pixelScale = props.pixelScale || 1;
@@ -178,7 +200,7 @@ namespace Camera {
     /**
      * Sets an offseted view in a larger frustum. This is useful for
      * - multi-window or multi-monitor/multi-machine setups
-     * - jittering the camera position for
+     * - jittering the camera position for sampling
      */
     export interface ViewOffset {
         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>
  */
@@ -55,14 +55,11 @@ namespace Viewport {
 
 //
 
-const NEAR_RANGE = 0;
-const FAR_RANGE = 1;
-
 const tmpVec4 = Vec4();
 
 /** 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
     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;
     }
 
-    // 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;
     return out;
 }
 
 /**
  * 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);
 }

+ 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 { DebugHelperParams } from './helper/bounding-sphere-helper';
 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 { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
 import { PickData } from './passes/pick';
@@ -38,6 +38,7 @@ import { StereoCamera, StereoCameraParams } from './camera/stereo';
 import { Helper } from './helper/helper';
 import { Passes } from './passes/passes';
 import { shallowEqual } from '../mol-util';
+import { MarkingParams } from './passes/marking';
 
 export const Canvas3DParams = {
     camera: PD.Group({
@@ -80,8 +81,10 @@ export const Canvas3DParams = {
 
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
+    marking: PD.Group(MarkingParams),
     renderer: PD.Group(RendererParams),
     trackball: PD.Group(TrackballControlsParams),
+    interaction: PD.Group(Canvas3dInteractionHelperParams),
     debug: PD.Group(DebugHelperParams),
     handle: PD.Group(HandleHelperParams),
 };
@@ -113,23 +116,27 @@ namespace Canvas3DContext {
         preserveDrawingBuffer: true,
         pixelScale: 1,
         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 function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
         const a = { ...DefaultAttribs, ...attribs };
-        const { antialias, preserveDrawingBuffer, pixelScale } = a;
+        const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const gl = getGLContext(canvas, {
             antialias,
             preserveDrawingBuffer,
             alpha: true, // the renderer requires an alpha channel
             depth: true, // the renderer requires a depth buffer
             premultipliedAlpha: true, // the renderer outputs PMA
+            preferWebGl1
         });
         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 passes = new Passes(webgl, attribs);
 
@@ -228,7 +235,7 @@ interface Canvas3D {
     /** Sets drawPaused = false without starting the built in animation loop */
     resume(): void
     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
 
     notifyDidDraw: boolean,
@@ -303,8 +310,8 @@ namespace Canvas3D {
         const renderer = Renderer.create(webgl, p.renderer);
         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);
 
         let cameraResetRequested = false;
@@ -337,7 +344,7 @@ namespace Canvas3D {
             return { loci, repr };
         }
 
-        function mark(reprLoci: Representation.Loci, action: MarkerAction) {
+        function mark(reprLoci: Representation.Loci, action: MarkerAction, noDraw = false) {
             const { repr, loci } = reprLoci;
             let changed = false;
             if (repr) {
@@ -347,7 +354,7 @@ namespace Canvas3D {
                 changed = helper.camera.mark(loci, action) || changed;
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
             }
-            if (changed) {
+            if (changed && !noDraw) {
                 scene.update(void 0, true);
                 helper.handle.scene.update(void 0, true);
                 helper.camera.scene.update(void 0, true);
@@ -390,7 +397,7 @@ namespace Canvas3D {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } 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;
                 didRender = true;
@@ -636,9 +643,11 @@ namespace Canvas3D {
                 viewport: p.viewport,
 
                 postprocessing: { ...p.postprocessing },
+                marking: { ...p.marking },
                 multiSample: { ...p.multiSample },
                 renderer: { ...renderer.props },
                 trackball: { ...controls.props },
+                interaction: { ...interactionHelper.props },
                 debug: { ...helper.debug.props },
                 handle: { ...helper.handle.props },
             };
@@ -729,7 +738,7 @@ namespace Canvas3D {
             resized,
             setProps: (properties, doNotRequestDraw = false) => {
                 const props: PartialCanvas3DProps = typeof properties === 'function'
-                    ? produce(getProps(), properties)
+                    ? produce(getProps(), properties as any)
                     : properties;
 
                 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.marking) Object.assign(p.marking, props.marking);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.renderer) renderer.setProps(props.renderer);
                 if (props.trackball) controls.setProps(props.trackball);
+                if (props.interaction) interactionHelper.setProps(props.interaction);
                 if (props.debug) helper.debug.setProps(props.debug);
                 if (props.handle) helper.handle.setProps(props.handle);
 
@@ -835,9 +846,6 @@ namespace Canvas3D {
                 height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
                 y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
                 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) {

+ 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 David Sehnal <david.sehnal@gmail.com>
@@ -10,7 +10,7 @@
 
 import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
 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 { Camera } from '../camera';
 import { absMax } from '../../mol-math/misc';
@@ -49,6 +49,9 @@ export const TrackballControlsParams = {
     minDistance: PD.Numeric(0.01, {}, { 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 }),
 
     /**
@@ -91,6 +94,7 @@ namespace TrackballControls {
         const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
         const wheelSub = input.wheel.subscribe(onWheel);
         const pinchSub = input.pinch.subscribe(onPinch);
+        const gestureSub = input.gesture.subscribe(onGesture);
 
         let _isInteracting = false;
 
@@ -390,25 +394,33 @@ namespace TrackballControls {
             _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;
 
-            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)) {
-                _zoomEnd[1] += delta * 0.0001;
+                _zoomEnd[1] += delta;
             }
             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)) {
                 _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() {
             if (disposed) return;
             disposed = true;
@@ -416,6 +428,7 @@ namespace TrackballControls {
             dragSub.unsubscribe();
             wheelSub.unsubscribe();
             pinchSub.unsubscribe();
+            gestureSub.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>; }
 
-    setProps (props: Partial<DebugHelperProps>) {
+    setProps(props: Partial<DebugHelperProps>) {
         Object.assign(this._props, props);
         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 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 { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 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 HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
@@ -19,6 +21,17 @@ type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
 
 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 {
     private ev = RxEventHelper.create();
 
@@ -48,6 +61,12 @@ export class Canvas3dInteractionHelper {
     private button: ButtonsType.Flag = ButtonsType.create(0);
     private modifiers: ModifiersKeys = ModifiersKeys.None;
 
+    readonly props: Canvas3dInteractionHelperProps;
+
+    setProps(props: Partial<Canvas3dInteractionHelperProps>) {
+        Object.assign(this.props, props);
+    }
+
     private identify(e: InputEvent, t: number) {
         const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
 
@@ -70,7 +89,7 @@ export class Canvas3dInteractionHelper {
         }
 
         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.prevLoci = loci;
             return;
@@ -78,13 +97,13 @@ export class Canvas3dInteractionHelper {
 
         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.prevLoci = loci;
     }
 
     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.currentIdentifyT = 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() {
         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;
             // console.log('drag');
             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;
             // console.log('move');
             this.move(x, y, buttons, button, modifiers);
@@ -166,7 +208,7 @@ export class Canvas3dInteractionHelper {
             this.leave();
         });
 
-        input.click.subscribe(({x, y, buttons, button, modifiers }) => {
+        input.click.subscribe(({ x, y, buttons, button, modifiers }) => {
             if (this.outsideViewport(x, y)) return;
             // console.log('click');
             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 { depthMerge_frag } from '../../mol-gl/shader/depth-merge.frag';
-import { copy_frag } from '../../mol-gl/shader/copy.frag';
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
+import { MarkingPass, MarkingProps } from './marking';
+import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -52,27 +53,6 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
     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 {
     private readonly drawTarget: RenderTarget
 
@@ -92,6 +72,7 @@ export class DrawPass {
     private copyFboPostprocessing: CopyRenderable
 
     private wboit: WboitPass | undefined
+    private readonly marking: MarkingPass
     readonly postprocessing: PostprocessingPass
     private readonly antialiasing: AntialiasingPass
 
@@ -122,11 +103,12 @@ export class DrawPass {
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.marking = new MarkingPass(webgl, width, height);
         this.postprocessing = new PostprocessingPass(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() {
@@ -162,6 +144,7 @@ export class DrawPass {
                 this.wboit.setSize(width, height);
             }
 
+            this.marking.setSize(width, height);
             this.postprocessing.setSize(width, height);
             this.antialiasing.setSize(width, height);
         }
@@ -281,10 +264,11 @@ export class DrawPass {
         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 postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
         const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
+        const markingEnabled = MarkingPass.isEnabled(markingProps);
 
         const { x, y, width, height } = camera.viewport;
         renderer.setViewport(x, y, width, height);
@@ -309,6 +293,22 @@ export class DrawPass {
             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) {
             helper.debug.syncVisibility();
             renderer.renderBlended(helper.debug.scene, camera, null);
@@ -338,15 +338,16 @@ export class DrawPass {
         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.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
+        renderer.setPixelRatio(this.webgl.pixelRatio);
 
         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 {
-            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 { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
+import { MarkingParams } from './marking';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
+    marking: PD.Group(MarkingParams),
 
     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._colorTarget = this.multiSamplePass.colorTarget;
         } 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);
         }
     }

+ 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 { Helper } from '../helper/helper';
 import { StereoCamera } from '../camera/stereo';
-
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { compose_frag } from '../../mol-gl/shader/compose.frag';
+import { MarkingProps } from './marking';
 
 const ComposeSchema = {
     ...QuadSchema,
@@ -55,7 +55,11 @@ export const 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 {
     static isEnabled(props: MultiSampleProps) {
@@ -119,7 +123,7 @@ export class MultiSamplePass {
         //
         // This manual approach to MSAA re-renders the scene once for
         // 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 baseSampleWeight = 1.0 / offsetList.length;
@@ -144,7 +148,7 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
             // 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
             composeTarget.bind();
@@ -186,7 +190,7 @@ export class MultiSamplePass {
         //
         // This manual approach to MSAA re-renders the scene once for
         // 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;
 
@@ -194,7 +198,7 @@ export class MultiSamplePass {
         const sampleWeight = 1.0 / offsetList.length;
 
         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.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             compose.update();
@@ -222,7 +226,7 @@ export class MultiSamplePass {
                 camera.update();
 
                 // 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
                 composeTarget.bind();
@@ -240,7 +244,7 @@ export class MultiSamplePass {
                 compose.render();
 
                 sampleIndex += 1;
-                if (sampleIndex >= offsetList.length ) break;
+                if (sampleIndex >= offsetList.length) break;
             }
         }
 
@@ -274,33 +278,33 @@ export class MultiSamplePass {
 
 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>
  */
@@ -11,6 +11,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Vec3 } from '../../mol-math/linear-algebra';
+import { spiral2d } from '../../mol-math/misc';
 import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing';
 import { Camera, ICamera } from '../camera';
 import { StereoCamera } from '../camera/stereo';
@@ -88,6 +89,7 @@ export class PickPass {
 
         this.groupPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'pickGroup');
+        // printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' })
 
         this.depthPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'depth');
@@ -111,6 +113,8 @@ export class PickHelper {
     private pickHeight: number
     private halfPickWidth: number
 
+    private spiral: [number, number][]
+
     private setupBuffers() {
         const bufferSize = this.pickWidth * this.pickHeight * 4;
         if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
@@ -138,6 +142,8 @@ export class PickHelper {
 
             this.setupBuffers();
         }
+
+        this.spiral = spiral2d(Math.round(this.pickScale * this.pickPadding));
     }
 
     private syncBuffers() {
@@ -177,6 +183,7 @@ export class PickHelper {
 
         renderer.setTransparentBackground(false);
         renderer.setDrawingBufferSize(this.pickPass.objectPickTarget.getWidth(), this.pickPass.objectPickTarget.getHeight());
+        renderer.setPixelRatio(this.pickScale);
 
         if (StereoCamera.is(camera)) {
             renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
@@ -192,7 +199,7 @@ export class PickHelper {
         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;
         if (webgl.isContextLost) return;
 
@@ -251,7 +258,14 @@ export class PickHelper {
         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);
     }
 }

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

@@ -154,10 +154,10 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
 }
 
 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++) {
         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[] {
-    let samples = [];
+    const samples = [];
     for (let i = 0; i < nSamples; i++) {
         let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
         scale = 0.1 + scale * (1.0 - 0.1);
@@ -193,6 +193,7 @@ const PostprocessingSchema = {
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
+    uOutlineColor: UniformSpec('v3'),
     uTransparentBackground: UniformSpec('b'),
 
     uMaxPossibleViewZDiff: UniformSpec('f'),
@@ -220,6 +221,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uFogNear: ValueCell.create(10000),
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
+        uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
         uTransparentBackground: ValueCell.create(false),
 
         uMaxPossibleViewZDiff: ValueCell.create(0.5),
@@ -241,7 +243,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
 export const PostprocessingParams = {
     occlusion: PD.MappedStatic('on', {
         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.' }),
             bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
             blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
@@ -252,6 +254,7 @@ export const PostprocessingParams = {
         on: PD.Group({
             scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
             threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
+            color: PD.Color(Color(0x000000)),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -314,7 +317,7 @@ export class PostprocessingPass {
 
         this.randomHemisphereVector = [];
         for (let i = 0; i < 256; i++) {
-            let v = Vec3();
+            const v = Vec3();
             v[0] = Math.random() * 2.0 - 1.0;
             v[1] = Math.random() * 2.0 - 1.0;
             v[2] = Math.random();
@@ -376,7 +379,7 @@ export class PostprocessingPass {
         const outlinesEnabled = props.outline.name === 'on';
         const occlusionEnabled = props.occlusion.name === 'on';
 
-        let invProjection = Mat4.identity();
+        const invProjection = Mat4.identity();
         Mat4.invert(invProjection, camera.projection);
 
         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.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);
             if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
             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';
 
 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>
 

+ 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 */
-export function resizeCanvas (canvas: HTMLCanvasElement, container: HTMLElement, scale = 1) {
+export function resizeCanvas(canvas: HTMLCanvasElement, container: HTMLElement, scale = 1) {
     let width = window.innerWidth;
     let height = window.innerHeight;
     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 }),
         });
         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._rowCount).toEqual(2);
         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);
     });
 
-    it ('remove', () => {
+    it('remove', () => {
         const list = create([1, 2, 3, 4]);
-        let fst = list.removeFirst();
+        const fst = list.removeFirst();
         expect(fst).toBe(1);
         expect(list.last!.value).toBe(4);
         expect(list.count).toBe(3);
         expect(toArray(list)).toEqual([2, 3, 4]);
 
-        let last = list.removeLast();
+        const last = list.removeLast();
         expect(last).toBe(4);
         expect(list.last!.value).toBe(3);
         expect(list.count).toBe(2);
         expect(toArray(list)).toEqual([2, 3]);
 
-        let n3 = list.find(3)!;
+        const n3 = list.find(3)!;
         list.remove(n3);
         expect(list.first!.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 min = Tuple.fst;
 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 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 {
     if (!xs.length) return Empty;
     // 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;
 }
 

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

@@ -22,9 +22,10 @@ export function ofRange(min: number, max: number) {
     return ret;
 }
 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 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 max(xs: Nums) { return xs[xs.length - 1]; }
 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) {
     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;
-    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;
     }
     return true;
@@ -340,7 +343,7 @@ export function deduplicate(xs: 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);
     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) */
     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 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;
     /** 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 {
     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_1 = new ArrayBuffer(8);
         return {
@@ -36,6 +36,12 @@ namespace IntTuple {
         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 {
         _float64[0] = t as any;
         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', () => {
     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, 3, 4);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
     });
 
     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, 2, 1);
         ChunkedArray.add2(arr, 5, 6);
@@ -23,13 +23,13 @@ describe('Chunked Array', () => {
     });
 
     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]);
         expect(ChunkedArray.compact(arr)).toEqual([1, 2, 3, 4]);
     });
 
     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, 3, 4);
         ChunkedArray.add2(arr, 5, 6);
@@ -39,7 +39,7 @@ describe('Chunked Array', () => {
     });
 
     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, 3, 4);
         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>) {
-        let nextSize = array.growBy * array.elementSize;
+        const nextSize = array.growBy * array.elementSize;
         array.currentSize = nextSize;
         array.currentIndex = 0;
         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 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);
 }
 
+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/
  */

+ 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>
  */
@@ -15,6 +15,8 @@ import { ColorNames } from '../../mol-util/color/names';
 import { NullLocation } from '../../mol-model/location';
 import { UniformColorTheme } from '../../mol-theme/color/uniform';
 import { UniformSizeTheme } from '../../mol-theme/size/uniform';
+import { smoothstep } from '../../mol-math/interpolate';
+import { Material } from '../../mol-util/material';
 
 export const VisualQualityInfo = {
     '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 CustomQualityParamInfo: PD.Info = {
         category: 'Custom Quality',
         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 function createSimple(colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData) {
         if (!transform) transform = createIdentityTransform();
         const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
         const theme: Theme = {
-            color: UniformColorTheme({}, { value: colorValue}),
-            size: UniformSizeTheme({}, { value: sizeValue})
+            color: UniformColorTheme({}, { value: colorValue }),
+            size: UniformSizeTheme({}, { value: sizeValue })
         };
         return { transform, locationIterator, theme };
     }
@@ -65,11 +103,18 @@ export namespace BaseGeometry {
             uVertexCount: ValueCell.create(counts.vertexCount),
             uGroupCount: ValueCell.create(counts.groupCount),
             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>) {
         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 {

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

@@ -6,7 +6,7 @@
 
 import { ValueCell } from '../../../mol-util';
 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 { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
@@ -25,6 +25,7 @@ import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
 import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
 import { RenderableState } from '../../../mol-gl/renderable';
+import { createEmptySubstance } from '../substance-data';
 
 export interface Cylinders {
     readonly kind: 'cylinders',
@@ -156,6 +157,8 @@ export namespace Cylinders {
         doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
         ignoreLight: 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
 
@@ -200,6 +203,7 @@ export namespace Cylinders {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
@@ -224,6 +228,7 @@ export namespace Cylinders {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
 
@@ -234,6 +239,8 @@ export namespace Cylinders {
             dDoubleSided: ValueCell.create(props.doubleSided),
             dIgnoreLight: ValueCell.create(props.ignoreLight),
             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.dIgnoreLight, props.ignoreLight);
         ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
+        ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
+        ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
     }
 
     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 { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume';
 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 { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
@@ -28,6 +28,7 @@ import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './
 import { createEmptyClipping } from '../clipping-data';
 import { Grid, Volume } from '../../../mol-model/volume';
 import { ColorNames } from '../../../mol-util/color/names';
+import { createEmptySubstance } from '../substance-data';
 
 const VolumeBox = Box();
 
@@ -129,7 +130,15 @@ export namespace 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']) {
@@ -238,6 +247,7 @@ export namespace DirectVolume {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const [x, y, z] = gridDimension.ref.value;
@@ -262,6 +272,7 @@ export namespace DirectVolume {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
@@ -311,8 +322,7 @@ export namespace DirectVolume {
     }
 
     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.dFlatShaded, props.flatShaded);
         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 { LocationIterator } from '../../../mol-geo/util/location-iterator';
 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 { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
 import { Theme } from '../../../mol-theme/theme';
@@ -26,6 +26,7 @@ import { fillSerial } from '../../../mol-util/array';
 import { createEmptyClipping } from '../clipping-data';
 import { NullLocation } from '../../../mol-model/location';
 import { QuadPositions } from '../../../mol-gl/compute/util';
+import { createEmptySubstance } from '../substance-data';
 
 const QuadIndices = new Uint32Array([
     0, 1, 2,
@@ -113,7 +114,10 @@ namespace 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 = {
@@ -142,6 +146,7 @@ namespace Image {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
@@ -154,6 +159,7 @@ namespace Image {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...transform,
             ...BaseGeometry.createValues(props, counts),

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

@@ -6,7 +6,7 @@
 
 import { ValueCell } from '../../../mol-util';
 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 { createColors } from '../color-data';
 import { createMarkers } from '../marker-data';
@@ -26,6 +26,7 @@ import { createEmptyOverpaint } from '../overpaint-data';
 import { createEmptyTransparency } from '../transparency-data';
 import { hashFnv32a } from '../../../mol-data/util';
 import { createEmptyClipping } from '../clipping-data';
+import { createEmptySubstance } from '../substance-data';
 
 /** Wide line */
 export interface Lines {
@@ -164,7 +165,7 @@ export namespace Lines {
 
     export const 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),
     };
     export type Params = typeof Params
@@ -210,6 +211,7 @@ export namespace Lines {
         const marker = createMarkers(instanceCount * groupCount);
         const overpaint = createEmptyOverpaint();
         const transparency = createEmptyTransparency();
+        const material = createEmptySubstance();
         const clipping = createEmptyClipping();
 
         const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
@@ -231,6 +233,7 @@ export namespace Lines {
             ...marker,
             ...overpaint,
             ...transparency,
+            ...material,
             ...clipping,
             ...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>
  */
@@ -9,20 +9,75 @@ import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 
 export type MarkerData = {
+    uMarker: ValueCell<number>,
     tMarker: ValueCell<TextureImage<Uint8Array>>
     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 {
     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) {
+        ValueCell.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, markers);
         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;
     } else {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(markers),
             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 };
 export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
     if (markerData) {
+        ValueCell.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, emptyMarkerTexture);
         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;
     } else {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(emptyMarkerTexture),
             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 tmpCylinderMatRot = Mat4();
 const tmpCylinderScale = Vec3();
-const tmpCylinderMatScale = Mat4();
 const tmpCylinderStart = 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
     if (matchDir) Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
     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);
-    Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);
+    Mat4.scale(m, tmpCylinderMatRot, tmpCylinderScale);
     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) {
     const { currentGroup, vertices, normals, indices, groups } = state;
 
-    let vertexCount = vertices.elementCount;
+    const vertexCount = vertices.elementCount;
     let offsetLength = 0;
 
     if (arrowHeight > 0) {

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio