Browse Source

Merge branch 'master' of https://github.com/molstar/molstar into ma-support

Alexander Rose 3 years ago
parent
commit
1964de1e44
100 changed files with 706 additions and 547 deletions
  1. 1 1
      .github/workflows/node.yml
  2. 3 1
      .vscode/settings.json
  3. 20 7
      CHANGELOG.md
  4. 5 0
      docs/interesting-pdb-entries.md
  5. 18 10
      package-lock.json
  6. 5 3
      package.json
  7. 1 1
      src/apps/docking-viewer/index.ts
  8. 1 1
      src/apps/docking-viewer/viewport.tsx
  9. 1 1
      src/apps/viewer/index.ts
  10. 4 4
      src/examples/basic-wrapper/index.ts
  11. 6 6
      src/examples/proteopedia-wrapper/index.ts
  12. 1 1
      src/extensions/anvil/behavior.ts
  13. 1 1
      src/extensions/dnatco/confal-pyramids/behavior.ts
  14. 1 1
      src/extensions/dnatco/confal-pyramids/util.ts
  15. 1 1
      src/extensions/geo-export/controls.ts
  16. 2 2
      src/extensions/geo-export/ui.tsx
  17. 10 7
      src/extensions/mp4-export/controls.ts
  18. 2 2
      src/extensions/mp4-export/ui.tsx
  19. 2 2
      src/extensions/pdbe/structure-quality-report/behavior.ts
  20. 1 1
      src/extensions/pdbe/structure-quality-report/prop.ts
  21. 1 1
      src/extensions/rcsb/assembly-symmetry/behavior.ts
  22. 1 1
      src/extensions/rcsb/assembly-symmetry/prop.ts
  23. 1 1
      src/extensions/rcsb/assembly-symmetry/ui.tsx
  24. 2 2
      src/extensions/rcsb/validation-report/behavior.ts
  25. 1 1
      src/extensions/rcsb/validation-report/prop.ts
  26. 6 6
      src/mol-canvas3d/camera.ts
  27. 1 1
      src/mol-canvas3d/camera/stereo.ts
  28. 8 3
      src/mol-canvas3d/canvas3d.ts
  29. 8 8
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  30. 5 5
      src/mol-canvas3d/helper/camera-helper.ts
  31. 4 4
      src/mol-canvas3d/helper/handle-helper.ts
  32. 3 3
      src/mol-canvas3d/helper/helper.ts
  33. 16 16
      src/mol-canvas3d/passes/draw.ts
  34. 1 1
      src/mol-canvas3d/passes/fxaa.ts
  35. 9 9
      src/mol-canvas3d/passes/image.ts
  36. 5 5
      src/mol-canvas3d/passes/marking.ts
  37. 9 7
      src/mol-canvas3d/passes/multi-sample.ts
  38. 3 3
      src/mol-canvas3d/passes/passes.ts
  39. 19 19
      src/mol-canvas3d/passes/pick.ts
  40. 19 19
      src/mol-canvas3d/passes/postprocessing.ts
  41. 5 5
      src/mol-canvas3d/passes/smaa.ts
  42. 4 4
      src/mol-canvas3d/passes/wboit.ts
  43. 2 2
      src/mol-data/int/sorted-ranges.ts
  44. 4 4
      src/mol-data/util/combination.ts
  45. 3 3
      src/mol-data/util/interval-iterator.ts
  46. 29 29
      src/mol-geo/geometry/text/font-atlas.ts
  47. 1 1
      src/mol-geo/geometry/texture-mesh/texture-mesh.ts
  48. 1 1
      src/mol-geo/util/marching-cubes/algorithm.ts
  49. 5 5
      src/mol-math/geometry/boundary-helper.ts
  50. 1 1
      src/mol-math/geometry/lookup3d/grid.ts
  51. 11 11
      src/mol-math/graph/inter-unit-graph.ts
  52. 6 6
      src/mol-model-formats/structure/common/component.ts
  53. 9 9
      src/mol-model-formats/structure/common/entity.ts
  54. 2 2
      src/mol-model-formats/structure/common/property.ts
  55. 2 2
      src/mol-model-props/common/custom-property.ts
  56. 1 1
      src/mol-model-props/computed/interactions/common.ts
  57. 2 2
      src/mol-model-props/integrative/pair-restraints.ts
  58. 4 4
      src/mol-model/sequence/alignment/alignment.ts
  59. 16 16
      src/mol-model/sequence/sequence.ts
  60. 6 1
      src/mol-model/structure/model/model.ts
  61. 7 1
      src/mol-model/structure/model/properties/atomic/hierarchy.ts
  62. 5 2
      src/mol-model/structure/model/properties/utils/atomic-derived.ts
  63. 15 1
      src/mol-model/structure/model/properties/utils/atomic-index.ts
  64. 6 0
      src/mol-model/structure/model/types.ts
  65. 1 1
      src/mol-model/structure/structure/structure.ts
  66. 9 9
      src/mol-model/structure/structure/unit/bonds.ts
  67. 1 1
      src/mol-model/structure/structure/unit/rings.ts
  68. 2 2
      src/mol-model/structure/structure/util/unit-transforms.ts
  69. 5 5
      src/mol-plugin-state/formats/registry.ts
  70. 36 23
      src/mol-plugin-state/helpers/structure-selection-query.ts
  71. 4 4
      src/mol-plugin-state/manager/interactivity.ts
  72. 4 4
      src/mol-plugin-state/manager/loci-label.ts
  73. 1 1
      src/mol-plugin-state/manager/structure/component.ts
  74. 1 1
      src/mol-plugin-state/manager/structure/focus.ts
  75. 2 2
      src/mol-plugin-state/manager/structure/hierarchy.ts
  76. 2 2
      src/mol-plugin-state/manager/structure/selection.ts
  77. 2 2
      src/mol-plugin-state/manager/volume/hierarchy.ts
  78. 1 1
      src/mol-plugin-ui/base.tsx
  79. 10 10
      src/mol-plugin-ui/controls.tsx
  80. 5 5
      src/mol-plugin-ui/controls/action-menu.tsx
  81. 8 8
      src/mol-plugin-ui/controls/color.tsx
  82. 9 9
      src/mol-plugin-ui/controls/common.tsx
  83. 5 5
      src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx
  84. 58 58
      src/mol-plugin-ui/controls/parameters.tsx
  85. 17 17
      src/mol-plugin-ui/controls/slider.tsx
  86. 8 14
      src/mol-plugin-ui/hooks/use-behavior.ts
  87. 4 4
      src/mol-plugin-ui/left-panel.tsx
  88. 68 5
      src/mol-plugin-ui/plugin.tsx
  89. 2 2
      src/mol-plugin-ui/sequence.tsx
  90. 3 3
      src/mol-plugin-ui/sequence/chain.ts
  91. 1 1
      src/mol-plugin-ui/sequence/element.ts
  92. 5 5
      src/mol-plugin-ui/sequence/hetero.ts
  93. 6 6
      src/mol-plugin-ui/sequence/polymer.ts
  94. 8 8
      src/mol-plugin-ui/sequence/sequence.tsx
  95. 15 0
      src/mol-plugin-ui/skin/base/components/misc.scss
  96. 3 3
      src/mol-plugin-ui/state/animation.tsx
  97. 6 6
      src/mol-plugin-ui/state/common.tsx
  98. 15 15
      src/mol-plugin-ui/state/snapshots.tsx
  99. 11 11
      src/mol-plugin-ui/state/tree.tsx
  100. 13 13
      src/mol-plugin-ui/structure/components.tsx

+ 1 - 1
.github/workflows/node.yml

@@ -9,7 +9,7 @@ jobs:
     - uses: actions/checkout@v2
     - uses: actions/setup-node@v2
       with:
-        node-version: 14
+        node-version: 17
     - run: npm ci
     - run: sudo apt-get install xvfb
     - name: Lint

+ 3 - 1
.vscode/settings.json

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

+ 20 - 7
CHANGELOG.md

@@ -6,12 +6,9 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
-- 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
+- Enable temporal multi-sampling by default
+    - Fix flickering during marking with camera at rest
+- Enable ``aromaticBonds`` in structure representations by default
 - Add ``PluginConfig.Structure.DefaultRepresentationPreset``
 - Add ModelArchive schema extensions (e.g., AlphaFold uses it for the pLDDT score)
 - Add ModelArchive option in DownloadStructure action
@@ -24,6 +21,22 @@ Note that since we don't clearly distinguish between a public and private interf
     - pLDDT & qmean score: coloring, repr presets, molql symbol, loci labels (including avg for mutli-residue selections)
     - pLDDT: selection query
 
+## [v3.0.0-dev.5] - 2021-12-16
+
+- Fix initial camera reset not triggering for some entries.
+
+## [v3.0.0-dev.4] - 2021-12-14
+
+- 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
+- Add drag and drop overlay
+- Safari 15.1 - 15.3 WebGL 2 support workaround
+- [Breaking] Move ``react`` and ``react-dom`` to ``peerDependencies``. This might break some builds.
+
 ## [v3.0.0-dev.3] - 2021-12-4
 
 - Fix OBJ and USDZ export
@@ -36,7 +49,7 @@ Note that since we don't clearly distinguish between a public and private interf
 
 - 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
+    - ``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

+ 5 - 0
docs/interesting-pdb-entries.md

@@ -29,6 +29,11 @@
 * Long linear sugar chain (4HG6)
 * Anisotropic B-factors/Ellipsoids (1EJG)
 * NOS bridges (LYS-CSO in 7B0L, 6ZWJ, 6ZWH)
+* Non-polymer components in polymer entities
+    * PN2 in 1F80
+    * ACE (many, e.g. 5AGU, 1E1X)
+    * ACY in 7ABY
+    * NH2 (many, e.g. 6Y13)
 
 Assembly symmetries
 * 5M30 (Assembly 1, C3 local and pseudo)

+ 18 - 10
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "molstar",
-  "version": "3.0.0-dev.3",
+  "version": "3.0.0-dev.5",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "molstar",
-      "version": "3.0.0-dev.3",
+      "version": "3.0.0-dev.5",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^2.0.10",
@@ -27,8 +27,6 @@
         "immer": "^9.0.7",
         "immutable": "^3.8.2",
         "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",
@@ -85,6 +83,10 @@
       },
       "optionalDependencies": {
         "gl": "^4.9.2"
+      },
+      "peerDependencies": {
+        "react": "^17.0.2",
+        "react-dom": "^17.0.2"
       }
     },
     "node_modules/@babel/code-frame": {
@@ -11292,6 +11294,7 @@
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
       "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+      "peer": true,
       "dependencies": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -11304,6 +11307,7 @@
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
       "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+      "peer": true,
       "dependencies": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
@@ -11864,6 +11868,7 @@
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
       "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+      "peer": true,
       "dependencies": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -12522,9 +12527,9 @@
       }
     },
     "node_modules/swagger-ui-dist": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.1.tgz",
-      "integrity": "sha512-xvPMEpOCbd5S9Z1pJT/L/K/C2yAX89ZWJDxEECkVW1dENsKFLbu7sRY7b6UBqLXuTUfdM5owlgPrpwr24CRonA=="
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz",
+      "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
     },
     "node_modules/swap-case": {
       "version": "2.0.2",
@@ -22606,6 +22611,7 @@
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz",
       "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==",
+      "peer": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -22615,6 +22621,7 @@
       "version": "17.0.2",
       "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
       "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==",
+      "peer": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1",
@@ -23039,6 +23046,7 @@
       "version": "0.20.2",
       "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz",
       "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==",
+      "peer": true,
       "requires": {
         "loose-envify": "^1.1.0",
         "object-assign": "^4.1.1"
@@ -23553,9 +23561,9 @@
       }
     },
     "swagger-ui-dist": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.1.tgz",
-      "integrity": "sha512-xvPMEpOCbd5S9Z1pJT/L/K/C2yAX89ZWJDxEECkVW1dENsKFLbu7sRY7b6UBqLXuTUfdM5owlgPrpwr24CRonA=="
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.1.3.tgz",
+      "integrity": "sha512-WvfPSfAAMlE/sKS6YkW47nX/hA7StmhYnAHc6wWCXNL0oclwLj6UXv0hQCkLnDgvebi0MEV40SJJpVjKUgH1IQ=="
     },
     "swap-case": {
       "version": "2.0.2",

+ 5 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "3.0.0-dev.3",
+  "version": "3.0.0-dev.5",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -146,14 +146,16 @@
     "immer": "^9.0.7",
     "immutable": "^3.8.2",
     "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"
   },
+  "peerDependencies": {
+    "react": "^17.0.2",
+    "react-dom": "^17.0.2"
+  },
   "optionalDependencies": {
     "gl": "^4.9.2"
   }

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

@@ -54,7 +54,7 @@ const DefaultViewerOptions = {
 };
 
 class Viewer {
-    plugin: PluginUIContext
+    plugin: PluginUIContext;
 
     constructor(elementOrId: string | HTMLElement, colors = [Color(0x992211), Color(0xDDDDDD)], showButtons = true) {
         const o = { ...DefaultViewerOptions, ...{

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

@@ -230,7 +230,7 @@ export class ViewportComponent extends PluginUIComponent {
 
     set = async (preset: StructureRepresentationPresetProvider) => {
         await this._set(this.plugin.managers.structure.hierarchy.selection.structures, preset);
-    }
+    };
 
     structurePreset = () => this.set(StructurePreset);
     illustrativePreset = () => this.set(IllustrativePreset);

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

@@ -99,7 +99,7 @@ const DefaultViewerOptions = {
 type ViewerOptions = typeof DefaultViewerOptions;
 
 export class Viewer {
-    plugin: PluginUIContext
+    plugin: PluginUIContext;
 
     constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
         const o = { ...DefaultViewerOptions, ...options };

+ 4 - 4
src/examples/basic-wrapper/index.ts

@@ -95,7 +95,7 @@ class BasicWrapper {
             loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: { direction: 'forward' } } }); },
             stop: () => this.plugin.managers.animation.stop()
         }
-    }
+    };
 
     coloring = {
         applyStripes: async () => {
@@ -119,7 +119,7 @@ class BasicWrapper {
                 }
             });
         }
-    }
+    };
 
     interactivity = {
         highlightOn: () => {
@@ -137,7 +137,7 @@ class BasicWrapper {
         clearHighlight: () => {
             this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci: EmptyLoci });
         }
-    }
+    };
 
     tests = {
         staticSuperposition: async () => {
@@ -168,7 +168,7 @@ class BasicWrapper {
             PluginCommands.Toast.Hide(this.plugin, { key: 'toast-1' });
             PluginCommands.Toast.Hide(this.plugin, { key: 'toast-2' });
         }
-    }
+    };
 }
 
 (window as any).BasicMolStarWrapper = new BasicWrapper();

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

@@ -270,7 +270,7 @@ class MolStarProteopediaWrapper {
     camera = {
         toggleSpin: () => this.toggleSpin(),
         resetPosition: () => PluginCommands.Camera.Reset(this.plugin, { })
-    }
+    };
 
     private animateModelIndexTargetFps() {
         return Math.max(1, this.animate.modelIndex.targetFps | 0);
@@ -285,7 +285,7 @@ class MolStarProteopediaWrapper {
             loop: () => { this.plugin.managers.animation.play(AnimateModelIndex, { duration: { name: 'computed', params: { targetFps: this.animateModelIndexTargetFps() } }, mode: { name: 'loop', params: { direction: 'forward' } } }); },
             stop: () => this.plugin.managers.animation.stop()
         }
-    }
+    };
 
     coloring = {
         evolutionaryConservation: async (params?: { sequence?: boolean, het?: boolean, keepStyle?: boolean }) => {
@@ -306,7 +306,7 @@ class MolStarProteopediaWrapper {
 
             await PluginCommands.State.Update(this.plugin, { state, tree });
         }
-    }
+    };
 
     private experimentalDataElement?: Element = void 0;
     experimentalData = {
@@ -330,7 +330,7 @@ class MolStarProteopediaWrapper {
                 this.experimentalDataElement = void 0;
             }
         }
-    }
+    };
 
     hetGroups = {
         reset: () => {
@@ -397,7 +397,7 @@ class MolStarProteopediaWrapper {
             const snapshot = this.plugin.canvas3d!.camera.getFocus(sphere.center, radius);
             PluginCommands.Camera.SetSnapshot(this.plugin, { snapshot, durationMs: 250 });
         }
-    }
+    };
 
     snapshot = {
         get: (params?: PluginState.SnapshotParams) => {
@@ -420,7 +420,7 @@ class MolStarProteopediaWrapper {
             }
         }
 
-    }
+    };
 }
 
 (window as any).MolStarProteopediaWrapper = MolStarProteopediaWrapper;

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

@@ -30,7 +30,7 @@ export const ANVILMembraneOrientation = PluginBehavior.create<{ autoAttach: bool
         description: 'Data calculated with ANVIL algorithm.'
     },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
-        private provider = MembraneOrientationProvider
+        private provider = MembraneOrientationProvider;
 
         register(): void {
             DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);

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

@@ -66,7 +66,7 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
                 /* TODO: Implement this */
                 return void 0;
             }
-        }
+        };
 
         register(): void {
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);

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

@@ -114,7 +114,7 @@ export namespace ConfalPyramidsUtil {
             this.modelNum = unit.model.modelNum;
         }
 
-        protected readonly data: CPT.PyramidsData
+        protected readonly data: CPT.PyramidsData;
         protected readonly hasMultipleModels: boolean;
         protected readonly entryId: string;
         protected readonly modelNum: number;

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

@@ -29,7 +29,7 @@ export const GeometryParams = {
 export class GeometryControls extends PluginComponent {
     readonly behaviors = {
         params: this.ev.behavior<PD.Values<typeof GeometryParams>>(PD.getDefaultValues(GeometryParams))
-    }
+    };
 
     private getFilename() {
         const models = this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Model)).map(s => s.obj!.data);

+ 2 - 2
src/extensions/geo-export/ui.tsx

@@ -85,7 +85,7 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         } finally {
             this.setState({ busy: false });
         }
-    }
+    };
 
     viewInAR = async () => {
         try {
@@ -104,5 +104,5 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
         } finally {
             this.setState({ busy: false });
         }
-    }
+    };
 }

+ 10 - 7
src/extensions/mp4-export/controls.ts

@@ -31,7 +31,7 @@ export class Mp4Controls extends PluginComponent {
         canApply: this.ev.behavior<PluginStateAnimation.CanApply>({ canApply: false }),
         info: this.ev.behavior<Mp4AnimationInfo>({ width: 0, height: 0 }),
         params: this.ev.behavior<PD.Values<typeof Mp4AnimationParams>>(PD.getDefaultValues(Mp4AnimationParams))
-    }
+    };
 
     setCurrent(name?: string) {
         const anim = this.animations.find(a => a.name === name);
@@ -125,17 +125,20 @@ export class Mp4Controls extends PluginComponent {
         this.subscribe(this.plugin.canvas3d?.resized!, () => this.syncInfo());
         this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
 
-        this.subscribe(this.plugin.behaviors.state.isBusy, b => {
-            const anim = this.current;
-            if (!b && anim) {
-                this.behaviors.canApply.next(anim.anim.canApply?.(this.plugin) ?? { canApply: true });
-            }
-        });
+        this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));
+        this.subscribe(this.plugin.managers.snapshot.events.changed, b => this.updateCanApply(b));
 
         this.sync();
         this.syncInfo();
     }
 
+    private updateCanApply(b?: any) {
+        const anim = this.current;
+        if (!b && anim) {
+            this.behaviors.canApply.next(anim.anim.canApply?.(this.plugin) ?? { canApply: true });
+        }
+    }
+
     constructor(private plugin: PluginContext) {
         super();
 

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

@@ -108,7 +108,7 @@ export class Mp4EncoderUI extends CollapsableControls<{}, State> {
 
     save = () => {
         download(new Blob([this.state.data!.movie]), this.state.data!.filename);
-    }
+    };
 
     generate = async () => {
         try {
@@ -119,5 +119,5 @@ export class Mp4EncoderUI extends CollapsableControls<{}, State> {
             console.error(e);
             this.setState({ busy: false });
         }
-    }
+    };
 }

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

@@ -21,7 +21,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
     },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
 
-        private provider = StructureQualityReportProvider
+        private provider = StructureQualityReportProvider;
 
         private labelPDBeValidation = {
             label: (loci: Loci): string | undefined => {
@@ -42,7 +42,7 @@ export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: bo
                     default: return void 0;
                 }
             }
-        }
+        };
 
         register(): void {
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);

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

@@ -38,7 +38,7 @@ namespace StructureQualityReport {
     }
 
     export function isApplicable(model?: Model): boolean {
-        return !!model && Model.isFromPdbArchive(model);
+        return !!model && Model.hasPdbId(model);
     }
 
     export const Schema = {

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

@@ -27,7 +27,7 @@ export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean
         description: 'Assembly Symmetry data calculated with BioJava, obtained via RCSB PDB.'
     },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
-        private provider = AssemblySymmetryProvider
+        private provider = AssemblySymmetryProvider;
 
         register(): void {
             this.ctx.state.data.actions.add(InitAssemblySymmetry3D);

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

@@ -53,7 +53,7 @@ export namespace AssemblySymmetry {
     export function isApplicable(structure?: Structure): boolean {
         return (
             !!structure && structure.models.length === 1 &&
-            Model.isFromPdbArchive(structure.models[0]) &&
+            Model.hasPdbId(structure.models[0]) &&
             isBiologicalAssembly(structure)
         );
     }

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

@@ -118,7 +118,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
 
     paramsOnChange = (options: AssemblySymmetryProps) => {
         this.updateAssemblySymmetry(options);
-    }
+    };
 
     get hasAssemblySymmetry3D() {
         return !this.pivot.cell.parent || !!StateSelection.findTagInSubtree(this.pivot.cell.parent.tree, this.pivot.cell.transform.ref, AssemblySymmetry.Tag.Representation);

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

@@ -30,7 +30,7 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         description: 'Data from wwPDB Validation Report, obtained via RCSB PDB.'
     },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
-        private provider = ValidationReportProvider
+        private provider = ValidationReportProvider;
 
         private labelProvider = {
             label: (loci: Loci): string | undefined => {
@@ -41,7 +41,7 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
                     randomCoilIndexLabel(loci)
                 ].filter(l => !!l).join('</br>');
             }
-        }
+        };
 
         register(): void {
             DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);

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

@@ -92,7 +92,7 @@ namespace ValidationReport {
     }
 
     export function isApplicable(model?: Model): boolean {
-        return !!model && Model.isFromPdbArchive(model);
+        return !!model && Model.hasPdbId(model);
     }
 
     export function fromXml(xml: XMLDocument, model: Model): ValidationReport {

+ 6 - 6
src/mol-canvas3d/camera.ts

@@ -37,7 +37,7 @@ class Camera implements ICamera {
     readonly projectionView: Mat4 = Mat4.identity();
     readonly inverseProjectionView: Mat4 = Mat4.identity();
 
-    private pixelScale: number
+    private pixelScale: number;
     get pixelRatio() {
         const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
         return dpr * this.pixelScale;
@@ -47,11 +47,11 @@ class Camera implements ICamera {
     readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
     readonly viewOffset = Camera.ViewOffset();
 
-    near = 1
-    far = 10000
-    fogNear = 5000
-    fogFar = 10000
-    zoom = 1
+    near = 1;
+    far = 10000;
+    fogNear = 5000;
+    fogFar = 10000;
+    zoom = 1;
 
     readonly transition: CameraTransitionManager = new CameraTransitionManager(this);
     readonly stateChanged = new BehaviorSubject<Partial<Camera.Snapshot>>(this.state);

+ 1 - 1
src/mol-canvas3d/camera/stereo.ts

@@ -33,7 +33,7 @@ class StereoCamera {
         return this.parent.viewOffset;
     }
 
-    private props: StereoCameraProps
+    private props: StereoCameraProps;
 
     constructor(private parent: Camera, props: Partial<StereoCameraProps> = {}) {
         this.props = { ...DefaultStereoCameraProps, ...props };

+ 8 - 3
src/mol-canvas3d/canvas3d.ts

@@ -395,7 +395,11 @@ namespace Canvas3D {
                 }
 
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
-                    multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
+                    if (!cameraChanged) {
+                        while (!multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p));
+                    } else {
+                        multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
+                    }
                 } else {
                     passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing, p.marking);
                 }
@@ -514,7 +518,7 @@ namespace Canvas3D {
 
             if (camera.transition.inTransition || nextCameraResetSnapshot) return false;
 
-            let cameraSphereOverlapsNone = true;
+            let cameraSphereOverlapsNone = true, isEmpty = true;
             Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius);
 
             // check if any renderable has moved outside of the old bounding sphere
@@ -525,12 +529,13 @@ namespace Canvas3D {
                 const b = r.values.boundingSphere.ref.value;
                 if (!b.radius) continue;
 
+                isEmpty = false;
                 const cameraDist = Vec3.distance(cameraSphere.center, b.center);
                 if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
                 if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
             }
 
-            return cameraSphereOverlapsNone;
+            return cameraSphereOverlapsNone || (!isEmpty && cameraSphere.radius <= 0.1);
         }
 
         const sceneCommitTimeoutMs = 250;

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

@@ -31,14 +31,14 @@ export type DebugHelperProps = PD.Values<DebugHelperParams>
 type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
 
 export class BoundingSphereHelper {
-    readonly scene: Scene
-
-    private readonly parent: Scene
-    private _props: DebugHelperProps
-    private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>()
-    private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>()
-    private sceneData: BoundingSphereData | undefined
-    private visibleSceneData: BoundingSphereData | undefined
+    readonly scene: Scene;
+
+    private readonly parent: Scene;
+    private _props: DebugHelperProps;
+    private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>();
+    private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>();
+    private sceneData: BoundingSphereData | undefined;
+    private visibleSceneData: BoundingSphereData | undefined;
 
     constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
         this.scene = Scene.create(ctx);

+ 5 - 5
src/mol-canvas3d/helper/camera-helper.ts

@@ -49,13 +49,13 @@ export type CameraHelperParams = typeof CameraHelperParams
 export type CameraHelperProps = PD.Values<CameraHelperParams>
 
 export class CameraHelper {
-    scene: Scene
-    camera: Camera
+    scene: Scene;
+    camera: Camera;
     props: CameraHelperProps = {
         axes: { name: 'off', params: {} }
-    }
+    };
 
-    private renderObject: GraphicsRenderObject | undefined
+    private renderObject: GraphicsRenderObject | undefined;
 
     constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
         this.scene = Scene.create(webgl);
@@ -109,7 +109,7 @@ export class CameraHelper {
             if (apply(Interval.ofSingleton(idx))) changed = true;
         }
         return changed;
-    }
+    };
 
     mark(loci: Loci, action: MarkerAction) {
         if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;

+ 4 - 4
src/mol-canvas3d/helper/handle-helper.ts

@@ -47,12 +47,12 @@ export type HandleHelperParams = typeof HandleHelperParams
 export type HandleHelperProps = PD.Values<HandleHelperParams>
 
 export class HandleHelper {
-    scene: Scene
+    scene: Scene;
     props: HandleHelperProps = {
         handle: { name: 'off', params: {} }
-    }
+    };
 
-    private renderObject: GraphicsRenderObject | undefined
+    private renderObject: GraphicsRenderObject | undefined;
 
     private _transform = Mat4();
     getBoundingSphere(out: Sphere3D, instanceId: number) {
@@ -117,7 +117,7 @@ export class HandleHelper {
             if (apply(Interval.ofSingleton(idx))) changed = true;
         }
         return changed;
-    }
+    };
 
     mark(loci: Loci, action: MarkerAction) {
         if (!MarkerActions.is(MarkerActions.Highlighting, action)) return false;

+ 3 - 3
src/mol-canvas3d/helper/helper.ts

@@ -23,9 +23,9 @@ export type HelperProps = PD.Values<typeof HelperParams>
 
 
 export class Helper {
-    readonly debug: BoundingSphereHelper
-    readonly camera: CameraHelper
-    readonly handle: HandleHelper
+    readonly debug: BoundingSphereHelper;
+    readonly camera: CameraHelper;
+    readonly handle: HandleHelper;
 
     constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
         const p = { ...DefaultHelperProps, ...props };

+ 16 - 16
src/mol-canvas3d/passes/draw.ts

@@ -54,27 +54,27 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
 }
 
 export class DrawPass {
-    private readonly drawTarget: RenderTarget
+    private readonly drawTarget: RenderTarget;
 
-    readonly colorTarget: RenderTarget
-    readonly depthTexture: Texture
-    readonly depthTexturePrimitives: Texture
+    readonly colorTarget: RenderTarget;
+    readonly depthTexture: Texture;
+    readonly depthTexturePrimitives: Texture;
 
-    readonly packedDepth: boolean
+    readonly packedDepth: boolean;
 
-    private depthTarget: RenderTarget
-    private depthTargetPrimitives: RenderTarget | null
-    private depthTargetVolumes: RenderTarget | null
-    private depthTextureVolumes: Texture
-    private depthMerge: DepthMergeRenderable
+    private depthTarget: RenderTarget;
+    private depthTargetPrimitives: RenderTarget | null;
+    private depthTargetVolumes: RenderTarget | null;
+    private depthTextureVolumes: Texture;
+    private depthMerge: DepthMergeRenderable;
 
-    private copyFboTarget: CopyRenderable
-    private copyFboPostprocessing: CopyRenderable
+    private copyFboTarget: CopyRenderable;
+    private copyFboPostprocessing: CopyRenderable;
 
-    private wboit: WboitPass | undefined
-    private readonly marking: MarkingPass
-    readonly postprocessing: PostprocessingPass
-    private readonly antialiasing: AntialiasingPass
+    private wboit: WboitPass | undefined;
+    private readonly marking: MarkingPass;
+    readonly postprocessing: PostprocessingPass;
+    private readonly antialiasing: AntialiasingPass;
 
     get wboitEnabled() {
         return !!this.wboit?.supported;

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

@@ -28,7 +28,7 @@ export const FxaaParams = {
 export type FxaaProps = PD.Values<typeof FxaaParams>
 
 export class FxaaPass {
-    private readonly renderable: FxaaRenderable
+    private readonly renderable: FxaaRenderable;
 
     constructor(private webgl: WebGLContext, input: Texture) {
         this.renderable = getFxaaRenderable(webgl, input);

+ 9 - 9
src/mol-canvas3d/passes/image.ts

@@ -30,19 +30,19 @@ export const ImageParams = {
 export type ImageProps = PD.Values<typeof ImageParams>
 
 export class ImagePass {
-    private _width = 0
-    private _height = 0
-    private _camera = new Camera()
+    private _width = 0;
+    private _height = 0;
+    private _camera = new Camera();
 
-    readonly props: ImageProps
+    readonly props: ImageProps;
 
-    private _colorTarget: RenderTarget
+    private _colorTarget: RenderTarget;
     get colorTarget() { return this._colorTarget; }
 
-    private readonly drawPass: DrawPass
-    private readonly multiSamplePass: MultiSamplePass
-    private readonly multiSampleHelper: MultiSampleHelper
-    private readonly helper: Helper
+    private readonly drawPass: DrawPass;
+    private readonly multiSamplePass: MultiSamplePass;
+    private readonly multiSampleHelper: MultiSampleHelper;
+    private readonly helper: Helper;
 
     get width() { return this._width; }
     get height() { return this._height; }

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

@@ -36,12 +36,12 @@ export class MarkingPass {
         return props.enabled;
     }
 
-    readonly depthTarget: RenderTarget
-    readonly maskTarget: RenderTarget
-    private readonly edgesTarget: RenderTarget
+    readonly depthTarget: RenderTarget;
+    readonly maskTarget: RenderTarget;
+    private readonly edgesTarget: RenderTarget;
 
-    private readonly edge: EdgeRenderable
-    private readonly overlay: OverlayRenderable
+    private readonly edge: EdgeRenderable;
+    private readonly overlay: OverlayRenderable;
 
     constructor(private webgl: WebGLContext, width: number, height: number) {
         this.depthTarget = webgl.createRenderTarget(width, height);

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

@@ -50,8 +50,8 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): Compose
 }
 
 export const MultiSampleParams = {
-    mode: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
-    sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }),
+    mode: PD.Select('temporal', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
+    sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }, { description: 'Take level^2 samples.' }),
 };
 export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
 
@@ -66,11 +66,11 @@ export class MultiSamplePass {
         return props.mode !== 'off';
     }
 
-    colorTarget: RenderTarget
+    colorTarget: RenderTarget;
 
-    private composeTarget: RenderTarget
-    private holdTarget: RenderTarget
-    private compose: ComposeRenderable
+    private composeTarget: RenderTarget;
+    private holdTarget: RenderTarget;
+    private compose: ComposeRenderable;
 
     constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
         const { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } = webgl.extensions;
@@ -317,15 +317,17 @@ JitterVectors.forEach(offsetList => {
 });
 
 export class MultiSampleHelper {
-    private sampleIndex = -2
+    private sampleIndex = -2;
 
     update(changed: boolean, props: MultiSampleProps) {
         if (changed) this.sampleIndex = -1;
         return props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
     }
 
+    /** Return `true` while more samples are needed */
     render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
         this.sampleIndex = this.multiSamplePass.render(this.sampleIndex, renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
+        return this.sampleIndex < 0;
     }
 
     constructor(private multiSamplePass: MultiSamplePass) {

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

@@ -10,9 +10,9 @@ import { MultiSamplePass } from './multi-sample';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 
 export class Passes {
-    readonly draw: DrawPass
-    readonly pick: PickPass
-    readonly multiSample: MultiSamplePass
+    readonly draw: DrawPass;
+    readonly pick: PickPass;
+    readonly multiSample: MultiSamplePass;
 
     constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
         const { gl } = webgl;

+ 19 - 19
src/mol-canvas3d/passes/pick.ts

@@ -25,13 +25,13 @@ const NullId = Math.pow(2, 24) - 2;
 export type PickData = { id: PickingId, position: Vec3 }
 
 export class PickPass {
-    readonly objectPickTarget: RenderTarget
-    readonly instancePickTarget: RenderTarget
-    readonly groupPickTarget: RenderTarget
-    readonly depthPickTarget: RenderTarget
+    readonly objectPickTarget: RenderTarget;
+    readonly instancePickTarget: RenderTarget;
+    readonly groupPickTarget: RenderTarget;
+    readonly depthPickTarget: RenderTarget;
 
-    private pickWidth: number
-    private pickHeight: number
+    private pickWidth: number;
+    private pickHeight: number;
 
     constructor(private webgl: WebGLContext, private drawPass: DrawPass, readonly pickBaseScale: number) {
         const pickScale = pickBaseScale / webgl.pixelRatio;
@@ -97,23 +97,23 @@ export class PickPass {
 }
 
 export class PickHelper {
-    dirty = true
+    dirty = true;
 
-    private objectBuffer: Uint8Array
-    private instanceBuffer: Uint8Array
-    private groupBuffer: Uint8Array
-    private depthBuffer: Uint8Array
+    private objectBuffer: Uint8Array;
+    private instanceBuffer: Uint8Array;
+    private groupBuffer: Uint8Array;
+    private depthBuffer: Uint8Array;
 
-    private viewport = Viewport()
+    private viewport = Viewport();
 
-    private pickScale: number
-    private pickX: number
-    private pickY: number
-    private pickWidth: number
-    private pickHeight: number
-    private halfPickWidth: number
+    private pickScale: number;
+    private pickX: number;
+    private pickY: number;
+    private pickWidth: number;
+    private pickHeight: number;
+    private halfPickWidth: number;
 
-    private spiral: [number, number][]
+    private spiral: [number, number][];
 
     private setupBuffers() {
         const bufferSize = this.pickWidth * this.pickHeight * 4;

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

@@ -271,29 +271,29 @@ export class PostprocessingPass {
         return props.occlusion.name === 'on' || props.outline.name === 'on';
     }
 
-    readonly target: RenderTarget
+    readonly target: RenderTarget;
 
-    private readonly outlinesTarget: RenderTarget
-    private readonly outlinesRenderable: OutlinesRenderable
+    private readonly outlinesTarget: RenderTarget;
+    private readonly outlinesRenderable: OutlinesRenderable;
 
-    private readonly randomHemisphereVector: Vec3[]
-    private readonly ssaoFramebuffer: Framebuffer
-    private readonly ssaoBlurFirstPassFramebuffer: Framebuffer
-    private readonly ssaoBlurSecondPassFramebuffer: Framebuffer
+    private readonly randomHemisphereVector: Vec3[];
+    private readonly ssaoFramebuffer: Framebuffer;
+    private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
+    private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
 
-    private readonly ssaoDepthTexture: Texture
-    private readonly ssaoDepthBlurProxyTexture: Texture
+    private readonly ssaoDepthTexture: Texture;
+    private readonly ssaoDepthBlurProxyTexture: Texture;
 
-    private readonly ssaoRenderable: SsaoRenderable
-    private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable
-    private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable
+    private readonly ssaoRenderable: SsaoRenderable;
+    private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
+    private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
 
-    private nSamples: number
-    private blurKernelSize: number
+    private nSamples: number;
+    private blurKernelSize: number;
 
-    private readonly renderable: PostprocessingRenderable
+    private readonly renderable: PostprocessingRenderable;
 
-    private ssaoScale: number
+    private ssaoScale: number;
     private calcSsaoScale() {
         // downscale ssao for high pixel-ratios
         return Math.min(1, 1 / this.webgl.pixelRatio);
@@ -543,9 +543,9 @@ export class AntialiasingPass {
         return props.antialiasing.name !== 'off';
     }
 
-    readonly target: RenderTarget
-    private readonly fxaa: FxaaPass
-    private readonly smaa: SmaaPass
+    readonly target: RenderTarget;
+    private readonly fxaa: FxaaPass;
+    private readonly smaa: SmaaPass;
 
     constructor(webgl: WebGLContext, private drawPass: DrawPass) {
         const { colorTarget } = drawPass;

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

@@ -31,12 +31,12 @@ export const SmaaParams = {
 export type SmaaProps = PD.Values<typeof SmaaParams>
 
 export class SmaaPass {
-    private readonly edgesTarget: RenderTarget
-    private readonly weightsTarget: RenderTarget
+    private readonly edgesTarget: RenderTarget;
+    private readonly weightsTarget: RenderTarget;
 
-    private readonly edgesRenderable: EdgesRenderable
-    private readonly weightsRenderable: WeightsRenderable
-    private readonly blendRenderable: BlendRenderable
+    private readonly edgesRenderable: EdgesRenderable;
+    private readonly weightsRenderable: WeightsRenderable;
+    private readonly blendRenderable: BlendRenderable;
 
     private _supported = false;
     get supported() {

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

@@ -45,11 +45,11 @@ function getEvaluateWboitRenderable(ctx: WebGLContext, wboitATexture: Texture, w
 //
 
 export class WboitPass {
-    private readonly renderable: EvaluateWboitRenderable
+    private readonly renderable: EvaluateWboitRenderable;
 
-    private readonly framebuffer: Framebuffer
-    private readonly textureA: Texture
-    private readonly textureB: Texture
+    private readonly framebuffer: Framebuffer;
+    private readonly textureA: Texture;
+    private readonly textureB: Texture;
 
     private _supported = false;
     get supported() {

+ 2 - 2
src/mol-data/int/sorted-ranges.ts

@@ -87,9 +87,9 @@ namespace SortedRanges {
     }
 
     export class Iterator<T extends number = number, I extends number = number> implements _Iterator<Segmentation.Segment<I>> {
-        private value: Segmentation.Segment<I> = { index: 0 as I, start: 0 as T, end: 0 as T }
+        private value: Segmentation.Segment<I> = { index: 0 as I, start: 0 as T, end: 0 as T };
 
-        private curIndex = 0
+        private curIndex = 0;
 
         hasNext: boolean = false;
 

+ 4 - 4
src/mol-data/util/combination.ts

@@ -28,11 +28,11 @@ function nextIndex(n: number) {
 };
 
 export class CombinationIterator<T> implements Iterator<ReadonlyArray<T>> {
-    private value: T[]
-    private index: number
-    private maxIndex: number
+    private value: T[];
+    private index: number;
+    private maxIndex: number;
 
-    size: number
+    size: number;
     hasNext: boolean = false;
 
     move() {

+ 3 - 3
src/mol-data/util/interval-iterator.ts

@@ -9,10 +9,10 @@ import { OrderedSet, Interval, Segmentation } from '../int';
 
 /** Emits a segment of length one for each element in the interval that is also in the set */
 export class IntervalIterator<I extends number = number> implements Iterator<Segmentation.Segment<I>> {
-    private value: Segmentation.Segment<I> = { index: 0 as I, start: 0, end: 0 }
+    private value: Segmentation.Segment<I> = { index: 0 as I, start: 0, end: 0 };
 
-    private curIndex = 0
-    private maxIndex = 0
+    private curIndex = 0;
+    private maxIndex = 0;
 
     hasNext: boolean = false;
 

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

@@ -39,35 +39,35 @@ export type FontAtlasMap = {
 }
 
 export class FontAtlas {
-    readonly props: Readonly<FontAtlasProps>
-    readonly mapped: { [k: string]: FontAtlasMap } = {}
-    readonly placeholder: FontAtlasMap
-    readonly texture: TextureImage<Uint8Array>
-
-    private scratchW = 0
-    private scratchH = 0
-    private currentX = 0
-    private currentY = 0
-    private readonly scratchData: Uint8Array
-
-    private readonly cutoff = 0.5
-    readonly buffer: number
-    private readonly radius: number
-
-    private gridOuter: Float64Array
-    private gridInner: Float64Array
-    private f: Float64Array
-    private d: Float64Array
-    private z: Float64Array
-    private v: Int16Array
-
-    private scratchCanvas: HTMLCanvasElement
-    private scratchContext: CanvasRenderingContext2D
-
-    readonly lineHeight: number
-
-    private readonly maxWidth: number
-    private readonly middle: number
+    readonly props: Readonly<FontAtlasProps>;
+    readonly mapped: { [k: string]: FontAtlasMap } = {};
+    readonly placeholder: FontAtlasMap;
+    readonly texture: TextureImage<Uint8Array>;
+
+    private scratchW = 0;
+    private scratchH = 0;
+    private currentX = 0;
+    private currentY = 0;
+    private readonly scratchData: Uint8Array;
+
+    private readonly cutoff = 0.5;
+    readonly buffer: number;
+    private readonly radius: number;
+
+    private gridOuter: Float64Array;
+    private gridInner: Float64Array;
+    private f: Float64Array;
+    private d: Float64Array;
+    private z: Float64Array;
+    private v: Int16Array;
+
+    private scratchCanvas: HTMLCanvasElement;
+    private scratchContext: CanvasRenderingContext2D;
+
+    readonly lineHeight: number;
+
+    private readonly maxWidth: number;
+    private readonly middle: number;
 
     constructor(props: Partial<FontAtlasProps> = {}) {
         const p = { ...PD.getDefaultValues(FontAtlasParams), ...props };

+ 1 - 1
src/mol-geo/geometry/texture-mesh/texture-mesh.ts

@@ -47,7 +47,7 @@ export interface TextureMesh {
 export namespace TextureMesh {
     export class DoubleBuffer {
         private index = 0;
-        private textures: ({ vertex: Texture, group: Texture, normal: Texture } | undefined)[] = []
+        private textures: ({ vertex: Texture, group: Texture, normal: Texture } | undefined)[] = [];
 
         get() {
             return this.textures[this.index];

+ 1 - 1
src/mol-geo/util/marching-cubes/algorithm.ts

@@ -72,7 +72,7 @@ export function computeMarchingCubesLines(params: MarchingCubesParams, lines?: L
 class MarchingCubesComputation {
     private size: number;
     private sliceSize: number;
-    private edgeFilter: number
+    private edgeFilter: number;
 
     private minX = 0; private minY = 0; private minZ = 0;
     private maxX = 0; private maxY = 0; private maxZ = 0;

+ 5 - 5
src/mol-math/geometry/boundary-helper.ts

@@ -12,12 +12,12 @@ import { Box3D } from './primitives/box3d';
 // implementing http://www.ep.liu.se/ecp/034/009/ecp083409.pdf
 
 export class BoundaryHelper {
-    private dir: Vec3[]
+    private dir: Vec3[];
 
-    private minDist: number[] = []
-    private maxDist: number[] = []
-    private extrema: Vec3[] = []
-    centroidHelper = new CentroidHelper()
+    private minDist: number[] = [];
+    private maxDist: number[] = [];
+    private extrema: Vec3[] = [];
+    centroidHelper = new CentroidHelper();
 
     private computeExtrema(i: number, p: Vec3) {
         const d = Vec3.dot(this.dir[i], p);

+ 1 - 1
src/mol-math/geometry/lookup3d/grid.ts

@@ -27,7 +27,7 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
     private ctx: QueryContext;
     boundary: Lookup3D['boundary'];
     buckets: GridLookup3D['buckets'];
-    result: Result<T>
+    result: Result<T>;
 
     find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T> {
         this.ctx.x = x;

+ 11 - 11
src/mol-math/graph/inter-unit-graph.ts

@@ -11,11 +11,11 @@ export { InterUnitGraph };
 
 class InterUnitGraph<UnitId extends number, VertexIndex extends number, EdgeProps extends InterUnitGraph.EdgePropsBase = {}> {
     /** Number of inter-unit edges */
-    readonly edgeCount: number
+    readonly edgeCount: number;
     /** Array of inter-unit edges */
-    readonly edges: ReadonlyArray<InterUnitGraph.Edge<UnitId, VertexIndex, EdgeProps>>
-    private readonly edgeKeyIndex: Map<string, number>
-    private readonly vertexKeyIndex: Map<string, number[]>
+    readonly edges: ReadonlyArray<InterUnitGraph.Edge<UnitId, VertexIndex, EdgeProps>>;
+    private readonly edgeKeyIndex: Map<string, number>;
+    private readonly vertexKeyIndex: Map<string, number[]>;
 
     /** Get an array of unit-pair-edges that are connected to the given unit */
     getConnectedUnits(unit: UnitId): ReadonlyArray<InterUnitGraph.UnitPairEdges<UnitId, VertexIndex, EdgeProps>> {
@@ -134,13 +134,13 @@ namespace InterUnitGraph {
 
 
     export class Builder<UnitId extends number, VertexIndex extends number, EdgeProps extends InterUnitGraph.EdgePropsBase = {}> {
-        private uA: UnitId
-        private uB: UnitId
-        private mapAB: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>
-        private mapBA: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>
-        private linkedA: UniqueArray<VertexIndex, VertexIndex>
-        private linkedB: UniqueArray<VertexIndex, VertexIndex>
-        private linkCount: number
+        private uA: UnitId;
+        private uB: UnitId;
+        private mapAB: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>;
+        private mapBA: Map<number, EdgeInfo<VertexIndex, EdgeProps>[]>;
+        private linkedA: UniqueArray<VertexIndex, VertexIndex>;
+        private linkedB: UniqueArray<VertexIndex, VertexIndex>;
+        private linkCount: number;
 
         private map = new Map<number, UnitPairEdges<UnitId, VertexIndex, EdgeProps>[]>();
 

+ 6 - 6
src/mol-model-formats/structure/common/component.ts

@@ -98,12 +98,12 @@ const CharmmIonComponents = (function () {
 })();
 
 export class ComponentBuilder {
-    private namesMap = new Map<string, string>()
-    private comps = new Map<string, Component>()
-    private ids: string[] = []
-    private names: string[] = []
-    private types: mmCIF_chemComp_schema['type']['T'][] = []
-    private mon_nstd_flags: mmCIF_chemComp_schema['mon_nstd_flag']['T'][] = []
+    private namesMap = new Map<string, string>();
+    private comps = new Map<string, Component>();
+    private ids: string[] = [];
+    private names: string[] = [];
+    private types: mmCIF_chemComp_schema['type']['T'][] = [];
+    private mon_nstd_flags: mmCIF_chemComp_schema['mon_nstd_flag']['T'][] = [];
 
     private set(c: Component) {
         this.comps.set(c.id, c);

+ 9 - 9
src/mol-model-formats/structure/common/entity.ts

@@ -14,16 +14,16 @@ export type EntityCompound = { chains: string[], description: string }
 type EntityType = 'water' | 'polymer' | 'non-polymer'
 
 export class EntityBuilder {
-    private count = 0
-    private ids: string[] = []
-    private types: EntityType[] = []
-    private descriptions: string[][] = []
+    private count = 0;
+    private ids: string[] = [];
+    private types: EntityType[] = [];
+    private descriptions: string[][] = [];
 
-    private compoundsMap = new Map<string, string>()
-    private namesMap = new Map<string, string>()
-    private heteroMap = new Map<string, string>()
-    private chainMap = new Map<string, string>()
-    private waterId?: string
+    private compoundsMap = new Map<string, string>();
+    private namesMap = new Map<string, string>();
+    private heteroMap = new Map<string, string>();
+    private chainMap = new Map<string, string>();
+    private waterId?: string;
 
     private set(type: EntityType, description: string) {
         this.count += 1;

+ 2 - 2
src/mol-model-formats/structure/common/property.ts

@@ -9,8 +9,8 @@ import { ModelFormat } from '../../format';
 import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
 
 class FormatRegistry<T> {
-    private map = new Map<ModelFormat['kind'], (model: Model) => T | undefined>()
-    private applicable = new Map<ModelFormat['kind'], (model: Model) => boolean>()
+    private map = new Map<ModelFormat['kind'], (model: Model) => T | undefined>();
+    private applicable = new Map<ModelFormat['kind'], (model: Model) => boolean>();
 
     add(kind: ModelFormat['kind'], obtain: (model: Model) => T | undefined, applicable?: (model: Model) => boolean) {
         this.map.set(kind, obtain);

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

@@ -42,8 +42,8 @@ namespace CustomProperty {
     }
 
     export class Registry<Data> {
-        private providers = OrderedMap<string, Provider<Data, any, any>>().asMutable()
-        private defaultAutoAttachValues = new Map<string, boolean>()
+        private providers = OrderedMap<string, Provider<Data, any, any>>().asMutable();
+        private defaultAutoAttachValues = new Map<string, boolean>();
 
         /** Get params for all applicable property providers */
         getParams(data?: Data) {

+ 1 - 1
src/mol-model-props/computed/interactions/common.ts

@@ -85,7 +85,7 @@ namespace InteractionsIntraContacts {
 
 export { InteractionsInterContacts };
 class InteractionsInterContacts extends InterUnitGraph<number, Features.FeatureIndex, InteractionsInterContacts.Props> {
-    private readonly elementKeyIndex: Map<string, number[]>
+    private readonly elementKeyIndex: Map<string, number[]>;
 
     getContactIndicesForElement(index: StructureElement.UnitIndex, unit: Unit): ReadonlyArray<number> {
         return this.elementKeyIndex.get(this.getElementKey(index, unit.id)) || [];

+ 2 - 2
src/mol-model-props/integrative/pair-restraints.ts

@@ -20,8 +20,8 @@ function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: Str
 }
 
 export class PairRestraints<T extends PairRestraint> {
-    readonly count: number
-    private readonly pairKeyIndices: Map<string, number[]>
+    readonly count: number;
+    private readonly pairKeyIndices: Map<string, number[]>;
 
     /** Indices into this.pairs */
     getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {

+ 4 - 4
src/mol-model/sequence/alignment/alignment.ts

@@ -21,11 +21,11 @@ export function align(seqA: ArrayLike<string>, seqB: ArrayLike<string>, options:
 }
 
 class Alignment {
-    readonly gapPenalty: number; readonly gapExtensionPenalty: number
-    readonly substMatrix: SubstitutionMatrixData | undefined
+    readonly gapPenalty: number; readonly gapExtensionPenalty: number;
+    readonly substMatrix: SubstitutionMatrixData | undefined;
 
-    readonly n: number; readonly m: number
-    readonly S: number[][] = []; readonly V: number[][] = []; readonly H: number[][] = []
+    readonly n: number; readonly m: number;
+    readonly S: number[][] = []; readonly V: number[][] = []; readonly H: number[][] = [];
 
     constructor(readonly seqA: ArrayLike<string>, readonly seqB: ArrayLike<string>, options: AlignmentOptions) {
         this.gapPenalty = options.gapPenalty;

+ 16 - 16
src/mol-model/sequence/sequence.ts

@@ -86,14 +86,14 @@ namespace Sequence {
     }
 
     class ResidueNamesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> {
-        public length: number
-        public code: Column<Alphabet>
-        public label: Column<string>
-        public seqId: Column<number>
-        public compId: Column<string>
-        public microHet: ReadonlyMap<number, string[]> = new Map()
-
-        private indexMap: Map<number, number>
+        public length: number;
+        public code: Column<Alphabet>;
+        public label: Column<string>;
+        public seqId: Column<number>;
+        public compId: Column<string>;
+        public microHet: ReadonlyMap<number, string[]> = new Map();
+
+        private indexMap: Map<number, number>;
         index(seqId: number) {
             return this.indexMap.get(seqId)!;
         }
@@ -158,14 +158,14 @@ namespace Sequence {
     }
 
     class SequenceRangesImpl<K extends Kind, Alphabet extends string> implements Base<K, Alphabet> {
-        public length: number
-        public code: Column<Alphabet>
-        public label: Column<string>
-        public seqId: Column<number>
-        public compId: Column<string>
-        public microHet: ReadonlyMap<number, string[]> = new Map()
-
-        private minSeqId: number
+        public length: number;
+        public code: Column<Alphabet>;
+        public label: Column<string>;
+        public seqId: Column<number>;
+        public compId: Column<string>;
+        public microHet: ReadonlyMap<number, string[]> = new Map();
+
+        private minSeqId: number;
         index(seqId: number) {
             return seqId - this.minSeqId;
         }

+ 6 - 1
src/mol-model/structure/model/model.ts

@@ -286,6 +286,11 @@ export namespace Model {
         for (let i = 0, il = db.database_2.database_id.rowCount; i < il; ++i) {
             if (db.database_2.database_id.value(i) === 'PDB') return true;
         }
+        return false;
+    }
+
+    export function hasPdbId(model: Model): boolean {
+        if (!MmcifFormat.is(model.sourceData)) return false;
         return (
             // 4 character PDB id
             model.entryId.match(/^[1-9][a-z0-9]{3,3}$/i) !== null ||
@@ -381,7 +386,7 @@ export namespace Model {
         const { db } = model.sourceData.data;
         return hasDensityMap(model) || (
             // check if from pdb archive but missing relevant meta data
-            isFromPdbArchive(model) && (
+            hasPdbId(model) && (
                 !db.exptl.method.isDefined ||
                 (isFromXray(model) && (
                     !db.pdbx_database_status.status_code_sf.isDefined ||

+ 7 - 1
src/mol-model/structure/model/properties/atomic/hierarchy.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 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>
@@ -206,6 +206,12 @@ export interface AtomicIndex {
      */
     findAtomsOnResidue(residueIndex: ResidueIndex, label_atom_ids: Set<string>): ElementIndex
 
+    /**
+     * Find element index of an atom on a given residue.
+     * @returns index or -1 if the atom is not present.
+     */
+    findElementOnResidue(residueIndex: ResidueIndex, type_symbol: ElementSymbol): ElementIndex
+
     // TODO: add indices that support comp_id?
 }
 

+ 5 - 2
src/mol-model/structure/model/properties/utils/atomic-derived.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,7 +7,7 @@
 import { AtomicData, AtomNumber } from '../atomic';
 import { AtomicIndex, AtomicDerivedData, AtomicSegments } from '../atomic/hierarchy';
 import { ElementIndex, ResidueIndex } from '../../indexing';
-import { MoleculeType, getMoleculeType, getComponentType, PolymerType, getPolymerType } from '../../types';
+import { MoleculeType, getMoleculeType, getComponentType, PolymerType, getPolymerType, isPolymer, ElementSymbol } from '../../types';
 import { getAtomIdForAtomRole } from '../../../../../mol-model/structure/util';
 import { ChemicalComponentMap } from '../common';
 import { isProductionMode } from '../../../../../mol-util/debug';
@@ -63,6 +63,9 @@ export function getAtomicDerivedData(data: AtomicData, segments: AtomicSegments,
         if (traceIndex === -1) {
             const coarseAtomId = getAtomIdForAtomRole(polyType, 'coarseBackbone');
             traceIndex = index.findAtomsOnResidue(i, coarseAtomId);
+            if (traceIndex === -1 && isPolymer(molType)) {
+                traceIndex = index.findElementOnResidue(i, ElementSymbol('C'));
+            }
         }
         traceElementIndex[i] = traceIndex;
 

+ 15 - 1
src/mol-model/structure/model/properties/utils/atomic-index.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -12,6 +12,7 @@ import { ChainIndex, ResidueIndex, EntityIndex, ElementIndex } from '../../index
 import { AtomicIndex, AtomicHierarchy } from '../atomic/hierarchy';
 import { cantorPairing } from '../../../../../mol-data/util';
 import { Column } from '../../../../../mol-data/db';
+import { ElementSymbol } from '../../types';
 
 function getResidueId(seq_id: number, ins_code: string) {
     if (!ins_code) return seq_id;
@@ -43,6 +44,7 @@ interface Mapping {
     label_atom_id: Column<string>,
     auth_atom_id: Column<string>,
     label_alt_id: Column<string>,
+    type_symbol: Column<ElementSymbol>,
     segments: AtomicSegments,
 
     chain_index_entity_index: EntityIndex[],
@@ -64,6 +66,7 @@ function createMapping(entities: Entities, data: AtomicData, segments: AtomicSeg
         label_atom_id: data.atoms.label_atom_id,
         auth_atom_id: data.atoms.auth_atom_id,
         label_alt_id: data.atoms.label_alt_id,
+        type_symbol: data.atoms.type_symbol,
         chain_index_entity_index: new Int32Array(data.chains._rowCount) as any,
         entity_index_label_asym_id: new Map(),
         chain_index_label_seq_id: new Map(),
@@ -173,6 +176,10 @@ class Index implements AtomicIndex {
         return findAtomByNames(this.residueOffsets[rI], this.residueOffsets[rI + 1], this.map.label_atom_id, label_atom_ids);
     }
 
+    findElementOnResidue(rI: ResidueIndex, type_symbol: ElementSymbol) {
+        return findAtomByElement(this.residueOffsets[rI], this.residueOffsets[rI + 1], this.map.type_symbol, type_symbol);
+    }
+
     constructor(private map: Mapping) {
         this.entityIndex = map.entities.getEntityIndex;
         this.residueOffsets = this.map.segments.residueAtomSegments.offsets;
@@ -201,6 +208,13 @@ function findAtomByNameAndAltLoc(start: ElementIndex, end: ElementIndex, nameDat
     return -1 as ElementIndex;
 }
 
+function findAtomByElement(start: ElementIndex, end: ElementIndex, data: Column<ElementSymbol>, typeSymbol: ElementSymbol): ElementIndex {
+    for (let i = start; i < end; i++) {
+        if (data.value(i) === typeSymbol) return i;
+    }
+    return -1 as ElementIndex;
+}
+
 export function getAtomicIndex(data: AtomicData, entities: Entities, segments: AtomicSegments): AtomicIndex {
     const map = createMapping(entities, data, segments);
 

+ 6 - 0
src/mol-model/structure/model/types.ts

@@ -321,6 +321,12 @@ export function getMoleculeType(compType: string, compId: string): MoleculeType
         if (SaccharideCompIdMap.has(compId)) {
             // trust our saccharide table more than given 'non-polymer' or 'other' component type
             return MoleculeType.Saccharide;
+        } else if (AminoAcidNames.has(compId)) {
+            return MoleculeType.Protein;
+        } else if (RnaBaseNames.has(compId)) {
+            return MoleculeType.RNA;
+        } else if (DnaBaseNames.has(compId)) {
+            return MoleculeType.DNA;
         } else {
             return MoleculeType.Other;
         }

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

@@ -938,7 +938,7 @@ namespace Structure {
 
     export class StructureBuilder {
         private units: Unit[] = [];
-        private invariantId = idFactory()
+        private invariantId = idFactory();
 
         private chainGroupId = -1;
         private inChainGroup = false;

+ 9 - 9
src/mol-model/structure/structure/unit/bonds.ts

@@ -180,18 +180,18 @@ namespace Bond {
     }
 
     export class ElementBondIterator implements Iterator<ElementBondData> {
-        private current: ElementBondData = {} as any
+        private current: ElementBondData = {} as any;
 
-        private structure: Structure
-        private unit: Unit.Atomic
-        private index: StructureElement.UnitIndex
+        private structure: Structure;
+        private unit: Unit.Atomic;
+        private index: StructureElement.UnitIndex;
 
-        private interBondIndices: ReadonlyArray<number>
-        private interBondCount: number
-        private interBondIndex: number
+        private interBondIndices: ReadonlyArray<number>;
+        private interBondCount: number;
+        private interBondIndex: number;
 
-        private intraBondEnd: number
-        private intraBondIndex: number
+        private intraBondEnd: number;
+        private intraBondIndex: number;
 
         hasNext: boolean;
         move(): ElementBondData {

+ 1 - 1
src/mol-model/structure/structure/unit/rings.ts

@@ -29,7 +29,7 @@ class UnitRings {
         readonly ringComponentIndex: ReadonlyArray<UnitRings.ComponentIndex>,
         readonly ringComponents: ReadonlyArray<ReadonlyArray<UnitRings.Index>>
     };
-    private _aromaticRings?: ReadonlyArray<UnitRings.Index>
+    private _aromaticRings?: ReadonlyArray<UnitRings.Index>;
 
     private get index() {
         if (this._index) return this._index;

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

@@ -12,8 +12,8 @@ import { fillIdentityTransform } from '../../../../mol-geo/geometry/transform-da
 const tmpMat = Mat4();
 
 export class StructureUnitTransforms {
-    private unitTransforms: Float32Array
-    private groupUnitTransforms: Float32Array[] = []
+    private unitTransforms: Float32Array;
+    private groupUnitTransforms: Float32Array[] = [];
     /** maps unit.id to offset of transform in unitTransforms */
     private unitOffsetMap = IntMap.Mutable<number>();
     private groupIndexMap = IntMap.Mutable<number>();

+ 5 - 5
src/mol-plugin-state/formats/registry.ts

@@ -14,11 +14,11 @@ import { BuiltInShapeFormats } from './shape';
 import { BuiltInStructureFormats } from './structure';
 
 export class DataFormatRegistry {
-    private _list: { name: string, provider: DataFormatProvider }[] = []
-    private _map = new Map<string, DataFormatProvider>()
-    private _extensions: Set<string> | undefined = undefined
-    private _binaryExtensions: Set<string> | undefined = undefined
-    private _options: [string, string, string][] | undefined = undefined
+    private _list: { name: string, provider: DataFormatProvider }[] = [];
+    private _map = new Map<string, DataFormatProvider>();
+    private _extensions: Set<string> | undefined = undefined;
+    private _binaryExtensions: Set<string> | undefined = undefined;
+    private _options: [string, string, string][] | undefined = undefined;
 
     get types(): [string, string][] {
         return this._list.map(e => [e.name, e.provider.label] as [string, string]);

+ 36 - 23
src/mol-plugin-state/helpers/structure-selection-query.ts

@@ -7,7 +7,7 @@
 
 import { CustomProperty } from '../../mol-model-props/common/custom-property';
 import { QueryContext, Structure, StructureQuery, StructureSelection, StructureProperties, StructureElement } from '../../mol-model/structure';
-import { BondType, NucleicBackboneAtoms, ProteinBackboneAtoms, SecondaryStructureType, AminoAcidNamesL, RnaBaseNames, DnaBaseNames, WaterNames, ElementSymbol } from '../../mol-model/structure/model/types';
+import { BondType, NucleicBackboneAtoms, ProteinBackboneAtoms, SecondaryStructureType, AminoAcidNamesL, RnaBaseNames, DnaBaseNames, WaterNames, ElementSymbol, PolymerNames } from '../../mol-model/structure/model/types';
 import { PluginContext } from '../../mol-plugin/context';
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { Expression } from '../../mol-script/language/expression';
@@ -320,33 +320,46 @@ const branchedConnectedOnly = StructureSelectionQuery('Connected to Carbohydrate
 ]), { category: StructureSelectionCategory.Internal, isHidden: true });
 
 const ligand = StructureSelectionQuery('Ligand', MS.struct.modifier.union([
-    MS.struct.combinator.merge([
-        MS.struct.modifier.union([
-            MS.struct.generator.atomGroups({
-                'entity-test': MS.core.logic.and([
-                    MS.core.logic.or([
-                        MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer']),
-                        MS.core.rel.neq([MS.ammp('entityPrdId'), ''])
-                    ]),
-                    MS.core.logic.not([MS.core.str.match([
-                        MS.re('(oligosaccharide|lipid|ion)', 'i'),
-                        MS.ammp('entitySubtype')
-                    ])])
+    MS.struct.modifier.exceptBy({
+        0: MS.struct.modifier.union([
+            MS.struct.combinator.merge([
+                MS.struct.modifier.union([
+                    MS.struct.generator.atomGroups({
+                        'entity-test': MS.core.logic.and([
+                            MS.core.logic.or([
+                                MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer']),
+                                MS.core.rel.neq([MS.ammp('entityPrdId'), ''])
+                            ]),
+                            MS.core.logic.not([MS.core.str.match([
+                                MS.re('(oligosaccharide|lipid|ion)', 'i'),
+                                MS.ammp('entitySubtype')
+                            ])])
+                        ]),
+                        'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                        'residue-test': MS.core.logic.not([
+                            MS.core.str.match([MS.re('saccharide', 'i'), MS.ammp('chemCompType')])
+                        ])
+                    })
                 ]),
-                'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'residue-test': MS.core.logic.not([
-                    MS.core.str.match([MS.re('saccharide', 'i'), MS.ammp('chemCompType')])
+                MS.struct.modifier.union([
+                    MS.struct.generator.atomGroups({
+                        'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
+                        'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
+                        'residue-test': _nonPolymerResidueTest
+                    })
                 ])
-            })
+            ]),
         ]),
-        MS.struct.modifier.union([
+        by: MS.struct.modifier.union([
             MS.struct.generator.atomGroups({
                 'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
                 'chain-test': MS.core.rel.eq([MS.ammp('objectPrimitive'), 'atomistic']),
-                'residue-test': _nonPolymerResidueTest
+                'residue-test': MS.core.set.has([
+                    MS.set(...SetUtils.toArray(PolymerNames)), MS.ammp('label_comp_id')
+                ])
             })
         ])
-    ]),
+    })
 ]), { category: StructureSelectionCategory.Type });
 
 // don't include branched entities as they have their own link representation
@@ -729,9 +742,9 @@ export const StructureSelectionQueries = {
 };
 
 export class StructureSelectionQueryRegistry {
-    list: StructureSelectionQuery[] = []
-    options: [StructureSelectionQuery, string, string][] = []
-    version = 1
+    list: StructureSelectionQuery[] = [];
+    options: [StructureSelectionQuery, string, string][] = [];
+    version = 1;
 
     add(q: StructureSelectionQuery) {
         this.list.push(q);

+ 4 - 4
src/mol-plugin-state/manager/interactivity.ts

@@ -36,7 +36,7 @@ class InteractivityManager extends StatefulPluginComponent<InteractivityManagerS
     readonly lociSelects: InteractivityManager.LociSelectManager;
     readonly lociHighlights: InteractivityManager.LociHighlightManager;
 
-    private _props = PD.getDefaultValues(InteractivityManager.Params)
+    private _props = PD.getDefaultValues(InteractivityManager.Params);
 
     readonly events = {
         propsUpdated: this.ev()
@@ -84,9 +84,9 @@ namespace InteractivityManager {
 
     export abstract class LociMarkManager {
         protected providers: LociMarkProvider[] = [];
-        protected sel: StructureSelectionManager
+        protected sel: StructureSelectionManager;
 
-        readonly props: Readonly<Props> = PD.getDefaultValues(Params)
+        readonly props: Readonly<Props> = PD.getDefaultValues(Params);
 
         setProps(props: Partial<Props>) {
             Object.assign(this.props, props);
@@ -141,7 +141,7 @@ namespace InteractivityManager {
                 this.mark(p, MarkerAction.RemoveHighlight, noRender);
             }
             this.prev.length = 0;
-        }
+        };
 
         highlight(current: Representation.Loci, applyGranularity = true) {
             const normalized = this.normalizedLoci(current, applyGranularity);

+ 4 - 4
src/mol-plugin-state/manager/loci-label.ts

@@ -41,7 +41,7 @@ export class LociLabelManager {
         this.showLabels();
     }
 
-    private locis: Representation.Loci[] = []
+    private locis: Representation.Loci[] = [];
 
     private mark(loci: Representation.Loci, action: MarkerAction) {
         const idx = this.locis.findIndex(l => Representation.Loci.areEqual(loci, l));
@@ -54,9 +54,9 @@ export class LociLabelManager {
         }
     }
 
-    private isDirty = false
-    private labels: LociLabel[] = []
-    private groupedLabels = new Map<string, LociLabel[]>()
+    private isDirty = false;
+    private labels: LociLabel[] = [];
+    private groupedLabels = new Map<string, LociLabel[]>();
 
     private showLabels() {
         this.ctx.behaviors.labels.highlight.next({ labels: this.getLabels() });

+ 1 - 1
src/mol-plugin-state/manager/structure/component.ts

@@ -42,7 +42,7 @@ interface StructureComponentManagerState {
 class StructureComponentManager extends StatefulPluginComponent<StructureComponentManagerState> {
     readonly events = {
         optionsUpdated: this.ev<undefined>()
-    }
+    };
 
     get currentStructures() {
         return this.plugin.managers.structure.hierarchy.selection.structures;

+ 1 - 1
src/mol-plugin-state/manager/structure/focus.ts

@@ -41,7 +41,7 @@ const HISTORY_CAPACITY = 8;
 export class StructureFocusManager extends StatefulPluginComponent<StructureFocusManagerState> {
     readonly events = {
         historyUpdated: this.ev<undefined>()
-    }
+    };
 
     readonly behaviors = {
         current: this.ev.behavior<FocusEntry | undefined>(void 0)

+ 2 - 2
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -25,7 +25,7 @@ export class StructureHierarchyManager extends PluginComponent {
             models: [] as ReadonlyArray<ModelRef>,
             structures: [] as ReadonlyArray<StructureRef>
         }
-    }
+    };
 
     readonly behaviors = {
         selection: this.ev.behavior({
@@ -34,7 +34,7 @@ export class StructureHierarchyManager extends PluginComponent {
             models: this.selection.models,
             structures: this.selection.structures
         })
-    }
+    };
 
     private get dataState() {
         return this.plugin.state.data;

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

@@ -45,9 +45,9 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
             remove: this.ev<StructureElement.Loci>(),
             clear: this.ev<undefined>()
         }
-    }
+    };
 
-    private referenceLoci: StructureElement.Loci | undefined
+    private referenceLoci: StructureElement.Loci | undefined;
 
     get entries() { return this.state.entries; }
     get additionsHistory() { return this.state.additionsHistory; }

+ 2 - 2
src/mol-plugin-state/manager/volume/hierarchy.ts

@@ -18,14 +18,14 @@ export class VolumeHierarchyManager extends PluginComponent {
 
         hierarchy: VolumeHierarchy(),
         selection: void 0 as VolumeRef | undefined
-    }
+    };
 
     readonly behaviors = {
         selection: this.ev.behavior({
             hierarchy: this.current,
             volume: this.selection
         })
-    }
+    };
 
     private get dataState() {
         return this.plugin.state.data;

+ 1 - 1
src/mol-plugin-ui/base.tsx

@@ -82,7 +82,7 @@ export type CollapsableState = {
 export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends PluginUIComponent<P & CollapsableProps, S & CollapsableState, SS> {
     toggleCollapsed = () => {
         this.setState({ isCollapsed: !this.state.isCollapsed } as (S & CollapsableState));
-    }
+    };
 
     componentDidUpdate(prevProps: P & CollapsableProps) {
         if (this.props.initiallyCollapsed !== undefined && prevProps.initiallyCollapsed !== this.props.initiallyCollapsed) {

+ 10 - 10
src/mol-plugin-ui/controls.tsx

@@ -26,7 +26,7 @@ import { PluginConfig } from '../mol-plugin/config';
 import { StructureSuperpositionControls } from './structure/superposition';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
-    state = { show: false, label: '' }
+    state = { show: false, label: '' };
 
     private update = () => {
         const state = this.plugin.state.data;
@@ -63,7 +63,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
 
         if (count > 1) label = '';
         this.setState({ show: count > 0, label });
-    }
+    };
 
     componentDidMount() {
         this.subscribe(this.plugin.state.data.events.changed, this.update);
@@ -100,7 +100,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
 }
 
 export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBusy: boolean, show: boolean }> {
-    state = { isBusy: false, show: true }
+    state = { isBusy: false, show: true };
 
     componentDidMount() {
         // TODO: this needs to be diabled when the state is updating!
@@ -147,23 +147,23 @@ export class StateSnapshotViewportControls extends PluginUIComponent<{}, { isBus
     change = (e: React.ChangeEvent<HTMLSelectElement>) => {
         if (e.target.value === 'none') return;
         this.update(e.target.value);
-    }
+    };
 
     prev = () => {
         const s = this.plugin.managers.snapshot;
         const id = s.getNextId(s.state.current, -1);
         if (id) this.update(id);
-    }
+    };
 
     next = () => {
         const s = this.plugin.managers.snapshot;
         const id = s.getNextId(s.state.current, 1);
         if (id) this.update(id);
-    }
+    };
 
     togglePlay = () => {
         this.plugin.managers.snapshot.togglePlay();
-    }
+    };
 
     render() {
         const snapshots = this.plugin.managers.snapshot;
@@ -212,7 +212,7 @@ export class AnimationViewportControls extends PluginUIComponent<{}, { isEmpty:
     stop = () => {
         this.plugin.managers.animation.stop();
         this.plugin.managers.snapshot.stop();
-    }
+    };
 
     render() {
         const isPlaying = this.plugin.managers.snapshot.state.isPlaying;
@@ -242,7 +242,7 @@ export class SelectionViewportControls extends PluginUIComponent {
     onMouseMove = (e: React.MouseEvent) => {
         // ignore mouse moves when no button is held
         if (e.buttons === 0) e.stopPropagation();
-    }
+    };
 
     render() {
         if (!this.plugin.selectionMode) return null;
@@ -253,7 +253,7 @@ export class SelectionViewportControls extends PluginUIComponent {
 }
 
 export class LociLabels extends PluginUIComponent<{}, { labels: ReadonlyArray<LociLabel> }> {
-    state = { labels: [] }
+    state = { labels: [] };
 
     componentDidMount() {
         this.subscribe(this.plugin.behaviors.labels.highlight, e => this.setState({ labels: e.labels }));

+ 5 - 5
src/mol-plugin-ui/controls/action-menu.tsx

@@ -11,7 +11,7 @@ import { Button, ControlGroup } from './common';
 import { CloseSvg, ArrowDropDownSvg, ArrowRightSvg, CheckSvg } from './icons';
 
 export class ActionMenu extends React.PureComponent<ActionMenu.Props> {
-    hide = () => this.props.onSelect(void 0)
+    hide = () => this.props.onSelect(void 0);
 
     render() {
         const cmd = this.props;
@@ -193,12 +193,12 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
         };
     }
 
-    state = Section.createState(this.props)
+    state = Section.createState(this.props);
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     componentDidUpdate(prevProps: SectionProps) {
         if (this.props.items !== prevProps.items || this.props.current !== prevProps.current) {
@@ -215,12 +215,12 @@ class Section extends React.PureComponent<SectionProps, SectionState> {
     selectAll = () => {
         const items = collectItems(this.props.items, []).filter(i => !i.selected);
         this.props.onSelect(items as any);
-    }
+    };
 
     selectNone = () => {
         const items = collectItems(this.props.items, []).filter(i => !!i.selected);
         this.props.onSelect(items as any);
-    }
+    };
 
     get multiselectHeader() {
         const { header, hasCurrent } = this.state;

+ 8 - 8
src/mol-plugin-ui/controls/color.tsx

@@ -18,7 +18,7 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
     state = {
         isExpanded: !!this.props.param.isExpanded,
         lightness: 0
-    }
+    };
 
     protected update(value: Color) {
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
@@ -27,7 +27,7 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     onClickSwatch = (e: React.MouseEvent<HTMLButtonElement>) => {
         const value = Color(+(e.currentTarget.getAttribute('data-color') || '0'));
@@ -35,33 +35,33 @@ export class CombinedColorControl extends React.PureComponent<ParamProps<PD.Colo
             if (!this.props.param.isExpanded) this.setState({ isExpanded: false });
             this.update(value);
         }
-    }
+    };
 
     onR = (v: number) => {
         const [, g, b] = Color.toRgb(this.props.value);
         const value = Color.fromRgb(v, g, b);
         if (value !== this.props.value) this.update(value);
-    }
+    };
 
     onG = (v: number) => {
         const [r, , b] = Color.toRgb(this.props.value);
         const value = Color.fromRgb(r, v, b);
         if (value !== this.props.value) this.update(value);
-    }
+    };
 
     onB = (v: number) => {
         const [r, g] = Color.toRgb(this.props.value);
         const value = Color.fromRgb(r, g, v);
         if (value !== this.props.value) this.update(value);
-    }
+    };
 
     onLighten = () => {
         this.update(Color.lighten(this.props.value, 0.1));
-    }
+    };
 
     onDarken = () => {
         this.update(Color.darken(this.props.value, 0.1));
-    }
+    };
 
     swatch() {
         return <div className='msp-combined-color-swatch'>

+ 9 - 9
src/mol-plugin-ui/controls/common.tsx

@@ -23,7 +23,7 @@ export class ControlGroup extends React.Component<{
     childrenClassName?: string,
     maxHeight?: string
 }, { isExpanded: boolean }> {
-    state = { isExpanded: !!this.props.initialExpanded }
+    state = { isExpanded: !!this.props.initialExpanded };
 
     headerClicked = () => {
         if (this.props.onHeaderClick) {
@@ -31,7 +31,7 @@ export class ControlGroup extends React.Component<{
         } else {
             this.setState({ isExpanded: !this.state.isExpanded });
         }
-    }
+    };
 
     render() {
         let groupClassName = this.props.hideOffset ? 'msp-control-group-children' : 'msp-control-group-children msp-control-offset';
@@ -84,12 +84,12 @@ export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>
     private delayHandle: any = void 0;
     private pendingValue: T | undefined = void 0;
 
-    state = { originalValue: '', value: '' }
+    state = { originalValue: '', value: '' };
 
     onBlur = () => {
         this.setState({ value: '' + this.state.originalValue });
         if (this.props.onBlur) this.props.onBlur();
-    }
+    };
 
     get isPending() { return typeof this.delayHandle !== 'undefined'; }
 
@@ -105,7 +105,7 @@ export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>
 
         this.props.onChange(this.pendingValue!);
         this.pendingValue = void 0;
-    }
+    };
 
     triggerChanged(formatted: string, converted: T) {
         this.clearTimeout();
@@ -138,7 +138,7 @@ export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>
             this.setState({ value: formatted }, () => this.triggerChanged(formatted, converted));
         }
 
-    }
+    };
 
     onKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if (e.charCode === 27 || e.keyCode === 27 || e.key === 'Escape') {
@@ -146,7 +146,7 @@ export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>
                 this.input.current.blur();
             }
         }
-    }
+    };
 
     onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if (e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter') {
@@ -160,7 +160,7 @@ export class TextInput<T = string> extends React.PureComponent<TextInputProps<T>
             if (this.props.onEnter) this.props.onEnter();
         }
         e.stopPropagation();
-    }
+    };
 
     static getDerivedStateFromProps(props: TextInputProps<any>, state: TextInputState) {
         const value = props.fromValue ? props.fromValue(props.value) : props.value;
@@ -322,7 +322,7 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
     onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
         e.currentTarget.blur();
         this.props.toggle();
-    }
+    };
 
     render() {
         const props = this.props;

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

@@ -117,15 +117,15 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS
 
     private handleKeyDown = (event: any) => {
         // TODO: set canSelectMultiple = true
-    }
+    };
 
     private handleKeyUp = (event: any) => {
         // TODO: SET canSelectMultiple = fasle
-    }
+    };
 
     private handleClick = (id: number) => (event: any) => {
         // TODO: add point to selected array
-    }
+    };
 
     private handleMouseDown = (id: number) => (event: any) => {
         if (id === 0 || id === this.state.points.length - 1) {
@@ -147,7 +147,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS
         this.updatedX = copyPoint[0];
         this.updatedY = copyPoint[1];
         this.selected = [id];
-    }
+    };
 
     private handleDrag(event: any) {
         if (this.selected === undefined) {
@@ -280,7 +280,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS
         this.setState({ points });
         this.change(points);
         event.stopPropagation();
-    }
+    };
 
     private handleLeave() {
         if (this.selected === undefined) {

+ 58 - 58
src/mol-plugin-ui/controls/parameters.tsx

@@ -45,7 +45,7 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent<
             const values = { ...this.props.values, [params.name]: params.value };
             this.props.onChangeValues(values, this.props.values);
         }
-    }
+    };
 
     renderGroup(group: ParamInfo[]) {
         if (group.length === 0) return null;
@@ -109,7 +109,7 @@ export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping:
         const values = { ...old, [p.name]: p.value };
         const t = this.props.mapping.update(values, this.plugin);
         this.props.mapping.apply(t, this.plugin);
-    }
+    };
 
     componentDidMount() {
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
@@ -289,7 +289,7 @@ export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<
 }
 
 export class BoolControl extends SimpleParam<PD.BooleanParam> {
-    onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); }
+    onClick = (e: React.MouseEvent<HTMLButtonElement>) => { this.update(!this.props.value); e.currentTarget.blur(); };
     renderControl() {
         return <button onClick={this.onClick} disabled={this.props.isDisabled}>
             <Icon svg={this.props.value ? CheckSvg : ClearSvg} />
@@ -303,7 +303,7 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
         isExpanded: false,
         isOverPoint: false,
         message: `${this.props.param.defaultValue.length} points`,
-    }
+    };
 
     onHover = (point?: Vec2) => {
         this.setState({ isOverPoint: !this.state.isOverPoint });
@@ -312,20 +312,20 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap
             return;
         }
         this.setState({ message: `${this.props.value.length} points` });
-    }
+    };
 
     onDrag = (point: Vec2) => {
         this.setState({ message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})` });
-    }
+    };
 
     onChange = (value: PD.LineGraph['defaultValue']) => {
         this.props.onChange({ name: this.props.name, param: this.props.param, value: value });
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     render() {
         const label = this.props.param.label || camelCaseToWords(this.props.name);
@@ -349,7 +349,7 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri
         const p = getPrecision(this.props.param.step || 0.01);
         value = parseFloat(value.toFixed(p));
         this.props.onChange({ param: this.props.param, name: this.props.name, value });
-    }
+    };
 
     render() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
@@ -365,7 +365,7 @@ export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeri
 }
 
 export class NumberRangeControl extends SimpleParam<PD.Numeric> {
-    onChange = (v: number) => { this.update(v); }
+    onChange = (v: number) => { this.update(v); };
     renderControl() {
         const value = typeof this.props.value === 'undefined' ? this.props.param.defaultValue : this.props.value;
         return <Slider value={value} min={this.props.param.min!} max={this.props.param.max!}
@@ -380,14 +380,14 @@ export class TextControl extends SimpleParam<PD.Text> {
         if (value !== this.props.value) {
             this.update(value);
         }
-    }
+    };
 
     onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
             if (this.props.onEnter) this.props.onEnter();
         }
         e.stopPropagation();
-    }
+    };
 
     renderControl() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
@@ -412,7 +412,7 @@ export class PureSelectControl extends React.PureComponent<ParamProps<PD.Select<
         } else {
             this.update(e.target.value);
         }
-    }
+    };
 
     render() {
         const isInvalid = this.props.value !== void 0 && !this.props.param.options.some(e => e[0] === this.props.value);
@@ -434,7 +434,7 @@ export class SelectControl extends React.PureComponent<ParamProps<PD.Select<stri
                 this.props.onChange({ param: this.props.param, name: this.props.name, value: item.value });
             });
         }
-    }
+    };
 
     toggle = () => this.setState({ showOptions: !this.state.showOptions });
 
@@ -500,7 +500,7 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<
                 this.props.onChange({ param: this.props.param, name: this.props.name, value: { ref: item.value } });
             });
         }
-    }
+    };
 
     toggle = () => this.setState({ showOptions: !this.state.showOptions });
 
@@ -540,12 +540,12 @@ export class ValueRefControl extends React.PureComponent<ParamProps<PD.ValueRef<
 }
 
 export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     components = {
         0: PD.Numeric(0, { step: this.props.param.step }, { label: 'Min' }),
         1: PD.Numeric(0, { step: this.props.param.step }, { label: 'Max' })
-    }
+    };
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -555,12 +555,12 @@ export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>
         const v = [...this.props.value];
         v[+name] = value;
         this.change(v);
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     render() {
         const v = this.props.value;
@@ -577,7 +577,7 @@ export class IntervalControl extends React.PureComponent<ParamProps<PD.Interval>
 }
 
 export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
-    onChange = (v: [number, number]) => { this.update(v); }
+    onChange = (v: [number, number]) => { this.update(v); };
     renderControl() {
         return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
             step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />;
@@ -587,7 +587,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
 export class ColorControl extends SimpleParam<PD.Color> {
     onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
         this.update(Color(parseInt(e.target.value)));
-    }
+    };
 
     stripStyle(): React.CSSProperties {
         return {
@@ -710,18 +710,18 @@ export class ColorListControl extends React.PureComponent<ParamProps<PD.ColorLis
 
         const preset = getColorListFromName(item.value as ColorListName);
         this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
-    }
+    };
 
     colorsChanged: ParamOnChange = ({ value }) => {
         this.update({
             kind: this.props.value.kind,
             colors: (value as (typeof _colorListHelpers)['ColorsParam']['defaultValue']).map(c => c.color)
         });
-    }
+    };
 
     isInterpolatedChanged: ParamOnChange = ({ value }) => {
         this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
-    }
+    };
 
     renderColors() {
         if (!this.state.show) return null;
@@ -779,17 +779,17 @@ export class OffsetColorListControl extends React.PureComponent<ParamProps<PD.Co
 
         const preset = getColorListFromName(item.value as ColorListName);
         this.update({ kind: preset.type !== 'qualitative' ? 'interpolate' : 'set', colors: preset.list });
-    }
+    };
 
     colorsChanged: ParamOnChange = ({ value }) => {
         const colors = (value as (typeof _colorListHelpers)['OffsetColorsParam']['defaultValue']).map(c => [c.color, c.offset] as [Color, number]);
         colors.sort((a, b) => a[1] - b[1]);
         this.update({ kind: this.props.value.kind, colors });
-    }
+    };
 
     isInterpolatedChanged: ParamOnChange = ({ value }) => {
         this.update({ kind: value ? 'interpolate' : 'set', colors: this.props.value.colors });
-    }
+    };
 
     renderColors() {
         if (!this.state.show) return null;
@@ -824,13 +824,13 @@ export class OffsetColorListControl extends React.PureComponent<ParamProps<PD.Co
 }
 
 export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     components = {
         0: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.x) || 'X' }),
         1: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.y) || 'Y' }),
         2: PD.Numeric(0, { step: this.props.param.step }, { label: (this.props.param.fieldLabels && this.props.param.fieldLabels.z) || 'Z' })
-    }
+    };
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -840,12 +840,12 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
         const v = Vec3.copy(Vec3.zero(), this.props.value);
         v[+name] = value;
         this.change(v);
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     render() {
         const v = this.props.value;
@@ -862,11 +862,11 @@ export class Vec3Control extends React.PureComponent<ParamProps<PD.Vec3>, { isEx
 }
 
 export class Mat4Control extends React.PureComponent<ParamProps<PD.Mat4>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     components = {
         json: PD.Text(JSON.stringify(Mat4()), { description: 'JSON array with 4x4 matrix in a column major (j * 4 + i indexing) format' })
-    }
+    };
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -881,12 +881,12 @@ export class Mat4Control extends React.PureComponent<ParamProps<PD.Mat4>, { isEx
         }
 
         this.change(v);
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     changeValue(idx: number) {
         return (v: number) => {
@@ -930,14 +930,14 @@ export class UrlControl extends SimpleParam<PD.UrlParam> {
         if (value !== Asset.getUrl(this.props.value || '')) {
             this.update(Asset.Url(value));
         }
-    }
+    };
 
     onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
             if (this.props.onEnter) this.props.onEnter();
         }
         e.stopPropagation();
-    }
+    };
 
     renderControl() {
         const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
@@ -960,7 +960,7 @@ export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
 
     onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
         this.change(e.target.files![0]);
-    }
+    };
 
     toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
 
@@ -1002,7 +1002,7 @@ export class FileListControl extends React.PureComponent<ParamProps<PD.FileListP
 
     onChangeFileList = (e: React.ChangeEvent<HTMLInputElement>) => {
         this.change(e.target.files!);
-    }
+    };
 
     toggleHelp = () => this.setState({ showHelp: !this.state.showHelp });
 
@@ -1040,7 +1040,7 @@ export class FileListControl extends React.PureComponent<ParamProps<PD.FileListP
 }
 
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     change(value: PD.MultiSelect<any>['defaultValue']) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -1057,7 +1057,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
         e.currentTarget.blur();
-    }
+    };
 
     render() {
         const current = this.props.value;
@@ -1080,7 +1080,7 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
 }
 
 export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>> & { inMapped?: boolean }, { isExpanded: boolean, showPresets: boolean, showHelp: boolean }> {
-    state = { isExpanded: !!this.props.param.isExpanded, showPresets: false, showHelp: false }
+    state = { isExpanded: !!this.props.param.isExpanded, showPresets: false, showHelp: false };
 
     change(value: any) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -1088,7 +1088,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
 
     onChangeParam: ParamOnChange = e => {
         this.change({ ...this.props.value, [e.name]: e.value });
-    }
+    };
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
     toggleShowPresets = () => this.setState({ showPresets: !this.state.showPresets });
@@ -1098,7 +1098,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
     onSelectPreset: ActionMenu.OnSelect = item => {
         this.setState({ showPresets: false });
         this.change(item?.value);
-    }
+    };
 
     pivotedPresets() {
         if (!this.props.param.presets) return null;
@@ -1197,11 +1197,11 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>
 }
 
 export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     // TODO: this could lead to a rare bug where the component is reused with different mapped control.
     // I think there are currently no cases where this could happen in the UI, but still need to watch out..
-    private valuesCache: { [name: string]: PD.Values<any> } = {}
+    private valuesCache: { [name: string]: PD.Values<any> } = {};
     private setValues(name: string, values: PD.Values<any>) {
         this.valuesCache[name] = values;
     }
@@ -1219,12 +1219,12 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
 
     onChangeName: ParamOnChange = e => {
         this.change({ name: e.value, params: this.getValues(e.value) });
-    }
+    };
 
     onChangeParam: ParamOnChange = e => {
         this.setValues(this.props.value.name, e.value);
         this.change({ name: this.props.value.name, params: e.value });
-    }
+    };
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
 
@@ -1283,11 +1283,11 @@ class ObjectListEditor extends React.PureComponent<ObjectListEditorProps, { curr
 
     onChangeParam: ParamOnChange = e => {
         this.setState({ current: { ...this.state.current, [e.name]: e.value } });
-    }
+    };
 
     apply = () => {
         this.props.apply(this.state.current);
-    }
+    };
 
     componentDidUpdate(prevProps: ObjectListEditorProps) {
         if (this.props.params !== prevProps.params || this.props.value !== prevProps.value) {
@@ -1312,7 +1312,7 @@ class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpand
     update = (v: object) => {
         // this.setState({ isExpanded: false }); // TODO auto update? mark changed state?
         this.props.actions.update(v, this.props.index);
-    }
+    };
 
     moveUp = () => {
         this.props.actions.move(this.props.index, -1);
@@ -1353,7 +1353,7 @@ class ObjectListItem extends React.PureComponent<ObjectListItemProps, { isExpand
 }
 
 export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectList>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: false };
 
     change(value: any) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
@@ -1390,7 +1390,7 @@ export class ObjectListControl extends React.PureComponent<ParamProps<PD.ObjectL
             }
             this.change(update);
         }
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
         this.setState({ isExpanded: !this.state.isExpanded });
@@ -1420,11 +1420,11 @@ export class ConditionedControl extends React.PureComponent<ParamProps<PD.Condit
 
     onChangeCondition: ParamOnChange = e => {
         this.change(this.props.param.conditionedValue(this.props.value, e.value));
-    }
+    };
 
     onChangeParam: ParamOnChange = e => {
         this.change(e.value);
-    }
+    };
 
     render() {
         const value = this.props.value;
@@ -1455,7 +1455,7 @@ export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converte
             param: this.props.param,
             value: this.props.param.toValue(e.value)
         });
-    }
+    };
 
     render() {
         const value = this.props.param.fromValue(this.props.value);
@@ -1472,14 +1472,14 @@ export class ScriptControl extends SimpleParam<PD.Script> {
         if (value !== this.props.value.expression) {
             this.update({ language: this.props.value.language, expression: value });
         }
-    }
+    };
 
     onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
         if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
             if (this.props.onEnter) this.props.onEnter();
         }
         e.stopPropagation();
-    }
+    };
 
     renderControl() {
         // TODO: improve!

+ 17 - 17
src/mol-plugin-ui/controls/slider.tsx

@@ -20,7 +20,7 @@ export class Slider extends React.Component<{
     onEnter?: () => void
 }, { isChanging: boolean, current: number }> {
 
-    state = { isChanging: false, current: 0 }
+    state = { isChanging: false, current: 0 };
 
     static getDerivedStateFromProps(props: { value: number }, state: { isChanging: boolean, current: number }) {
         if (state.isChanging || props.value === state.current) return null;
@@ -29,17 +29,17 @@ export class Slider extends React.Component<{
 
     begin = () => {
         this.setState({ isChanging: true });
-    }
+    };
 
     end = (v: number) => {
         this.setState({ isChanging: false });
         this.props.onChange(v);
-    }
+    };
 
     updateCurrent = (current: number) => {
         this.setState({ current });
         this.props.onChangeImmediate?.(current);
-    }
+    };
 
     updateManually = (v: number) => {
         this.setState({ isChanging: true });
@@ -50,12 +50,12 @@ export class Slider extends React.Component<{
         if (n > this.props.max) n = this.props.max;
 
         this.setState({ current: n, isChanging: true });
-    }
+    };
 
     onManualBlur = () => {
         this.setState({ isChanging: false });
         this.props.onChange(this.state.current);
-    }
+    };
 
     render() {
         let step = this.props.step;
@@ -85,7 +85,7 @@ export class Slider2 extends React.Component<{
     onEnter?: () => void
 }, { isChanging: boolean, current: [number, number] }> {
 
-    state = { isChanging: false, current: [0, 1] as [number, number] }
+    state = { isChanging: false, current: [0, 1] as [number, number] };
 
     static getDerivedStateFromProps(props: { value: [number, number] }, state: { isChanging: boolean, current: [number, number] }) {
         if (state.isChanging || (props.value[0] === state.current[0] && props.value[1] === state.current[1])) return null;
@@ -94,16 +94,16 @@ export class Slider2 extends React.Component<{
 
     begin = () => {
         this.setState({ isChanging: true });
-    }
+    };
 
     end = (v: [number, number]) => {
         this.setState({ isChanging: false });
         this.props.onChange(v);
-    }
+    };
 
     updateCurrent = (current: [number, number]) => {
         this.setState({ current });
-    }
+    };
 
     updateMax = (v: number) => {
         let n = v;
@@ -112,7 +112,7 @@ export class Slider2 extends React.Component<{
         else if (n < this.props.min) n = this.props.min;
         if (n > this.props.max) n = this.props.max;
         this.props.onChange([this.state.current[0], n]);
-    }
+    };
 
     updateMin = (v: number) => {
         let n = v;
@@ -121,7 +121,7 @@ export class Slider2 extends React.Component<{
         if (n > this.state.current[1]) n = this.state.current[1];
         else if (n > this.props.max) n = this.props.max;
         this.props.onChange([n, this.state.current[1]]);
-    }
+    };
 
     render() {
         let step = this.props.step;
@@ -371,7 +371,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         this.onStart(position);
         this.addDocumentEvents('mouse');
         pauseEvent(e);
-    }
+    };
 
     onMouseMove(e: MouseEvent) {
         const position = getMousePosition(this.props.vertical!, e);
@@ -462,7 +462,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
 
         const position = getTouchPosition(this.props.vertical!, e);
         this.onMove(e, position - this.dragOffset);
-    }
+    };
 
     onTouchStart = (e: TouchEvent) => {
         if (isNotTouchEvent(e)) return;
@@ -478,7 +478,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         this.onStart(position);
         this.addDocumentEvents('touch');
         pauseEvent(e);
-    }
+    };
 
     /**
      * Returns an array of possible slider points, taking into account both
@@ -536,7 +536,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         'touchend': (e: TouchEvent) => this.end('touch'),
         'mousemove': (e: MouseEvent) => this.onMouseMove(e),
         'mouseup': (e: MouseEvent) => this.end('mouse'),
-    }
+    };
 
     addDocumentEvents(type: 'touch' | 'mouse') {
         if (type === 'touch') {
@@ -552,7 +552,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
         const { min, max } = this.props;
         const ratio = (value - min) / (max - min);
         return ratio * 100;
-    }
+    };
 
     calcValue(offset: number) {
         const { vertical, min, max } = this.props;

+ 8 - 14
src/mol-plugin-ui/hooks/use-behavior.ts

@@ -1,10 +1,10 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-21 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { useEffect, useState } from 'react';
+import { useEffect, useRef, useState } from 'react';
 
 interface Behavior<T> {
     value: T;
@@ -16,26 +16,20 @@ export function useBehavior<T>(s: Behavior<T>): T;
 export function useBehavior<T>(s: Behavior<T> | undefined): T | undefined;
 // eslint-disable-next-line
 export function useBehavior<T>(s: Behavior<T> | undefined): T | undefined {
-    const [value, setValue] = useState(s?.value);
+    const [, next] = useState({});
+    const current = useRef<T>();
+    current.current = s?.value;
 
     useEffect(() => {
         if (!s) {
-            if (value !== void 0) setValue(void 0);
             return;
         }
-        let fst = true;
         const sub = s.subscribe((v) => {
-            if (fst) {
-                fst = false;
-                if (v !== value) setValue(v);
-            } else setValue(v);
+            if (current.current !== v) next({});
         });
 
-        return () => {
-            sub.unsubscribe();
-        };
-        // eslint-disable-next-line
+        return () => sub.unsubscribe();
     }, [s]);
 
-    return value;
+    return s?.value;
 }

+ 4 - 4
src/mol-plugin-ui/left-panel.tsx

@@ -47,7 +47,7 @@ export class LeftPanelControls extends PluginUIComponent<{}, { tab: LeftPanelTab
         if (this.plugin.layout.state.regionState.left !== 'full') {
             PluginCommands.Layout.Update(this.plugin, { state: { regionState: { ...this.plugin.layout.state.regionState, left: 'full' } } });
         }
-    }
+    };
 
     tabs: { [K in LeftPanelTabName]: JSX.Element } = {
         'none': <></>,
@@ -69,7 +69,7 @@ export class LeftPanelControls extends PluginUIComponent<{}, { tab: LeftPanelTab
             <SectionHeader icon={HelpOutlineSvg} title='Help' />
             <HelpContent />
         </>
-    }
+    };
 
     render() {
         const tab = this.state.tab;
@@ -119,7 +119,7 @@ class DataIcon extends PluginUIComponent<{ set: (tab: LeftPanelTabName) => void
 class FullSettings extends PluginUIComponent {
     private setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
         PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
-    }
+    };
 
     componentDidMount() {
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
@@ -158,7 +158,7 @@ class RemoveAllButton extends PluginUIComponent<{ }> {
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.State.RemoveObject(this.plugin, { state: this.plugin.state.data, ref: StateTransform.RootRef });
-    }
+    };
 
     render() {
         const count = this.plugin.state.data.tree.children.get(StateTransform.RootRef).size;

+ 68 - 5
src/mol-plugin-ui/plugin.tsx

@@ -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>
@@ -20,6 +20,8 @@ import { PluginCommands } from '../mol-plugin/commands';
 import { PluginUIContext } from './context';
 import { OpenFiles } from '../mol-plugin-state/actions/file';
 import { Asset } from '../mol-util/assets';
+import { BehaviorSubject } from 'rxjs';
+import { useBehavior } from './hooks/use-behavior';
 
 export class Plugin extends React.Component<{ plugin: PluginUIContext }, {}> {
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -133,19 +135,22 @@ class Layout extends PluginUIComponent {
                 visuals: true
             }));
         }
-    }
+    };
 
     onDragOver = (ev: React.DragEvent<HTMLDivElement>) => {
         ev.preventDefault();
-    }
+    };
+
+    private showDragOverlay = new BehaviorSubject(false);
+    onDragEnter = () => this.showDragOverlay.next(true);
 
     render() {
         const layout = this.plugin.layout.state;
         const controls = this.plugin.spec.components?.controls || {};
         const viewport = this.plugin.spec.components?.viewport?.view || DefaultViewport;
 
-        return <div className='msp-plugin' onDrop={this.onDrop} onDragOver={this.onDragOver}>
-            <div className={this.layoutClassName}>
+        return <div className='msp-plugin'>
+            <div className={this.layoutClassName} onDragEnter={this.onDragEnter}>
                 <div className={this.layoutVisibilityClassName}>
                     {this.region('main', viewport)}
                     {layout.showControls && controls.top !== 'none' && this.region('top', controls.top || SequenceView)}
@@ -154,11 +159,69 @@ class Layout extends PluginUIComponent {
                     {layout.showControls && controls.bottom !== 'none' && this.region('bottom', controls.bottom || Log)}
                 </div>
                 {!this.plugin.spec.components?.hideTaskOverlay && <OverlayTaskProgress />}
+                <DragOverlay plugin={this.plugin} showDragOverlay={this.showDragOverlay} />
             </div>
         </div>;
     }
 }
 
+function dropFiles(ev: React.DragEvent<HTMLDivElement>, plugin: PluginUIContext, showDragOverlay: BehaviorSubject<boolean>) {
+    ev.preventDefault();
+    ev.stopPropagation();
+    showDragOverlay.next(false);
+
+    const files: File[] = [];
+    if (ev.dataTransfer.items) {
+        // Use DataTransferItemList interface to access the file(s)
+        for (let i = 0; i < ev.dataTransfer.items.length; i++) {
+            if (ev.dataTransfer.items[i].kind !== 'file') continue;
+            const file = ev.dataTransfer.items[i].getAsFile();
+            if (file) files.push(file);
+        }
+    } else {
+        for (let i = 0; i < ev.dataTransfer.files.length; i++) {
+            const file = ev.dataTransfer.files[0];
+            if (file) files.push(file);
+        }
+    }
+
+    const sessions = files.filter(f => {
+        const fn = f.name.toLowerCase();
+        return fn.endsWith('.molx') || fn.endsWith('.molj');
+    });
+
+    if (sessions.length > 0) {
+        PluginCommands.State.Snapshots.OpenFile(plugin, { file: sessions[0] });
+    } else {
+        plugin.runTask(plugin.state.data.applyAction(OpenFiles, {
+            files: files.map(f => Asset.File(f)),
+            format: { name: 'auto', params: {} },
+            visuals: true
+        }));
+    }
+}
+
+function DragOverlay({ plugin, showDragOverlay }: { plugin: PluginUIContext, showDragOverlay: BehaviorSubject<boolean> }) {
+    const show = useBehavior(showDragOverlay);
+
+    const preventDrag = (e: React.DragEvent) => {
+        e.dataTransfer.dropEffect = 'copy';
+        e.preventDefault();
+        e.stopPropagation();
+    };
+
+    return <div
+        className='msp-drag-drop-overlay'
+        style={{ display: show ? 'flex' : 'none' }}
+        onDragEnter={preventDrag}
+        onDragOver={preventDrag}
+        onDragLeave={() => showDragOverlay.next(false)}
+        onDrop={e => dropFiles(e, plugin, showDragOverlay)}
+    >
+        Load File(s)
+    </div>;
+}
+
 export class ControlsWrapper extends PluginUIComponent {
     render() {
         const StructureTools = this.plugin.spec.components?.structureTools || DefaultStructureTools;

+ 2 - 2
src/mol-plugin-ui/sequence.tsx

@@ -219,7 +219,7 @@ type SequenceViewState = {
 }
 
 export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceViewMode }, SequenceViewState> {
-    state: SequenceViewState = { structureOptions: { options: [], all: [] }, structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '', mode: 'single' }
+    state: SequenceViewState = { structureOptions: { options: [], all: [] }, structure: Structure.Empty, structureRef: '', modelEntityId: '', chainGroupId: -1, operatorKey: '', mode: 'single' };
 
     componentDidMount() {
         if (this.plugin.state.data.select(StateSelection.Generators.rootsOfType(PSO.Molecule.Structure)).length > 0) this.setState(this.getInitialState());
@@ -358,7 +358,7 @@ export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceView
                 break;
         }
         this.setState(state);
-    }
+    };
 
     render() {
         if (this.getStructure(this.state.structureRef) === Structure.Empty) {

+ 3 - 3
src/mol-plugin-ui/sequence/chain.ts

@@ -12,9 +12,9 @@ import { ColorNames } from '../../mol-util/color/names';
 import { MarkerAction, applyMarkerAction } from '../../mol-util/marker-action';
 
 export class ChainSequenceWrapper extends SequenceWrapper<StructureUnit> {
-    private label: string
-    private unitIndices: Map<number, Interval<StructureElement.UnitIndex>>
-    private loci: StructureElement.Loci
+    private label: string;
+    private unitIndices: Map<number, Interval<StructureElement.UnitIndex>>;
+    private loci: StructureElement.Loci;
 
     residueLabel(seqIdx: number) {
         return this.label;

+ 1 - 1
src/mol-plugin-ui/sequence/element.ts

@@ -12,7 +12,7 @@ import { ColorNames } from '../../mol-util/color/names';
 import { MarkerAction, applyMarkerAction } from '../../mol-util/marker-action';
 
 export class ElementSequenceWrapper extends SequenceWrapper<StructureUnit> {
-    private unitIndices: Map<number, Interval<StructureElement.UnitIndex>>
+    private unitIndices: Map<number, Interval<StructureElement.UnitIndex>>;
 
     residueLabel(seqIdx: number) {
         return 'X';

+ 5 - 5
src/mol-plugin-ui/sequence/hetero.ts

@@ -12,11 +12,11 @@ import { ColorNames } from '../../mol-util/color/names';
 import { MarkerAction, applyMarkerAction } from '../../mol-util/marker-action';
 
 export class HeteroSequenceWrapper extends SequenceWrapper<StructureUnit> {
-    private readonly unitMap: Map<number, Unit>
-    private readonly sequence: string[]
-    private readonly sequenceIndices: Map<ResidueIndex, number>
-    private readonly residueIndices: Map<number, ResidueIndex>
-    private readonly seqToUnit: Map<number, Unit>
+    private readonly unitMap: Map<number, Unit>;
+    private readonly sequence: string[];
+    private readonly sequenceIndices: Map<ResidueIndex, number>;
+    private readonly residueIndices: Map<number, ResidueIndex>;
+    private readonly seqToUnit: Map<number, Unit>;
 
     residueLabel(seqIdx: number) {
         return this.sequence[seqIdx];

+ 6 - 6
src/mol-plugin-ui/sequence/polymer.ts

@@ -14,13 +14,13 @@ import { ColorNames } from '../../mol-util/color/names';
 import { MarkerAction, applyMarkerAction, applyMarkerActionAtPosition } from '../../mol-util/marker-action';
 
 export class PolymerSequenceWrapper extends SequenceWrapper<StructureUnit> {
-    private readonly unitMap: Map<number, Unit>
-    private readonly sequence: Sequence
-    private readonly missing: MissingResidues
-    private readonly observed: OrderedSet // sequences indices
+    private readonly unitMap: Map<number, Unit>;
+    private readonly sequence: Sequence;
+    private readonly missing: MissingResidues;
+    private readonly observed: OrderedSet; // sequences indices
 
-    private readonly modelNum: number
-    private readonly asymId: string
+    private readonly modelNum: number;
+    private readonly asymId: string;
 
     private seqId(seqIdx: number) {
         return this.sequence.seqId.value(seqIdx);

+ 8 - 8
src/mol-plugin-ui/sequence/sequence.tsx

@@ -34,12 +34,12 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
     private lociHighlightProvider = (loci: Representation.Loci, action: MarkerAction) => {
         const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
         if (changed) this.updateMarker();
-    }
+    };
 
     private lociSelectionProvider = (loci: Representation.Loci, action: MarkerAction) => {
         const changed = this.props.sequenceWrapper.markResidue(loci.loci, action);
         if (changed) this.updateMarker();
-    }
+    };
 
     private get sequenceNumberPeriod() {
         if (this.props.sequenceNumberPeriod !== undefined) {
@@ -104,9 +104,9 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
 
     contextMenu = (e: React.MouseEvent) => {
         e.preventDefault();
-    }
+    };
 
-    private mouseDownLoci: StructureElement.Loci | undefined = undefined
+    private mouseDownLoci: StructureElement.Loci | undefined = undefined;
 
     mouseDown = (e: React.MouseEvent) => {
         e.stopPropagation();
@@ -119,7 +119,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
 
         this.click(loci, buttons, button, modifiers);
         this.mouseDownLoci = loci;
-    }
+    };
 
     mouseUp = (e: React.MouseEvent) => {
         e.stopPropagation();
@@ -148,7 +148,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
             this.click(StructureElement.Loci.subtract(range, this.mouseDownLoci), buttons, button, modifiers);
         }
         this.mouseDownLoci = undefined;
-    }
+    };
 
     private getBackgroundColor(marker: number) {
         // TODO: make marker color configurable
@@ -262,7 +262,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
                 this.highlightQueue.next({ seqIdx, buttons, button, modifiers });
             }
         }
-    }
+    };
 
     mouseLeave = (e: React.MouseEvent) => {
         e.stopPropagation();
@@ -274,7 +274,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
         const button = getButton(e.nativeEvent);
         const modifiers = getModifiers(e.nativeEvent);
         this.highlightQueue.next({ seqIdx: -1, buttons, button, modifiers });
-    }
+    };
 
     render() {
         const sw = this.props.sequenceWrapper;

+ 15 - 0
src/mol-plugin-ui/skin/base/components/misc.scss

@@ -626,4 +626,19 @@
 .msp-list-unstyled {
     padding-left: 0;
     list-style: none;
+}
+
+.msp-drag-drop-overlay {
+    border: 12px dashed $font-color;
+    background: rgba(0, 0, 0, 0.36);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    font-size: 48px;
+    font-weight: bold;
 }

+ 3 - 3
src/mol-plugin-ui/state/animation.tsx

@@ -16,11 +16,11 @@ export class AnimationControls extends PluginUIComponent<{ onStart?: () => void
 
     updateParams: ParamOnChange = p => {
         this.plugin.managers.animation.updateParams({ [p.name]: p.value });
-    }
+    };
 
     updateCurrentParams: ParamOnChange = p => {
         this.plugin.managers.animation.updateCurrentParams({ [p.name]: p.value });
-    }
+    };
 
     startOrStop = () => {
         const anim = this.plugin.managers.animation;
@@ -29,7 +29,7 @@ export class AnimationControls extends PluginUIComponent<{ onStart?: () => void
             if (this.props.onStart) this.props.onStart();
             anim.start();
         }
-    }
+    };
 
     render() {
         const anim = this.plugin.managers.animation;

+ 6 - 6
src/mol-plugin-ui/state/common.tsx

@@ -129,7 +129,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
     private onEnter = () => {
         if (this.state.error) return;
         this.apply();
-    }
+    };
 
     private autoApplyHandle: number | undefined = void 0;
     private clearAutoApply() {
@@ -150,7 +150,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
                 }
             });
         }
-    }
+    };
 
     apply = async () => {
         this.clearAutoApply();
@@ -164,7 +164,7 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
             this.props.onApply?.();
             this.busy.next(false);
         }
-    }
+    };
 
     componentDidMount() {
         this.subscribe(this.plugin.behaviors.state.isBusy, b => {
@@ -177,17 +177,17 @@ abstract class TransformControlBase<P, S extends TransformControlBase.ComponentS
 
     refresh = () => {
         this.setState({ params: this.getInfo().initialValues, isInitial: true, error: void 0 });
-    }
+    };
 
     setDefault = () => {
         const info = this.getInfo();
         const params = PD.getDefaultValues(info.params);
         this.setState({ params, isInitial: PD.areEqual(info.params, params, info.initialValues), error: void 0 });
-    }
+    };
 
     toggleExpanded = () => {
         this.setState({ isCollapsed: !this.state.isCollapsed });
-    }
+    };
 
     renderApply() {
         const canApply = this.canApply();

+ 15 - 15
src/mol-plugin-ui/state/snapshots.tsx

@@ -45,12 +45,12 @@ export class StateExportImportControls extends PluginUIComponent<{ onAction?: ()
     downloadToFileJson = () => {
         this.props.onAction?.();
         PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'json' });
-    }
+    };
 
     downloadToFileZip = () => {
         this.props.onAction?.();
         PluginCommands.State.Snapshots.DownloadToFile(this.plugin, { type: 'zip' });
-    }
+    };
 
     open = (e: React.ChangeEvent<HTMLInputElement>) => {
         if (!e.target.files || !e.target.files[0]) {
@@ -60,7 +60,7 @@ export class StateExportImportControls extends PluginUIComponent<{ onAction?: ()
 
         this.props.onAction?.();
         PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: e.target.files[0] });
-    }
+    };
 
     render() {
         return <>
@@ -107,13 +107,13 @@ export class LocalStateSnapshots extends PluginUIComponent<
             name: this.state.params.name,
             description: this.state.params.description
         });
-    }
+    };
 
     updateParams = (params: PD.Values<typeof LocalStateSnapshots.Params>) => this.setState({ params });
 
     clear = () => {
         PluginCommands.State.Snapshots.Clear(this.plugin, {});
-    }
+    };
 
     shouldComponentUpdate(nextProps: any, nextState: any) {
         return !shallowEqualObjects(this.props, nextProps) || !shallowEqualObjects(this.state, nextState);
@@ -139,31 +139,31 @@ export class LocalStateSnapshotList extends PluginUIComponent<{}, {}> {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
         PluginCommands.State.Snapshots.Apply(this.plugin, { id });
-    }
+    };
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
         PluginCommands.State.Snapshots.Remove(this.plugin, { id });
-    }
+    };
 
     moveUp = (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
         PluginCommands.State.Snapshots.Move(this.plugin, { id, dir: -1 });
-    }
+    };
 
     moveDown = (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
         PluginCommands.State.Snapshots.Move(this.plugin, { id, dir: 1 });
-    }
+    };
 
     replace = (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
         if (!id) return;
         PluginCommands.State.Snapshots.Replace(this.plugin, { id });
-    }
+    };
 
     render() {
         const current = this.plugin.managers.snapshot.state.current;
@@ -250,7 +250,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             this.plugin.log.error('Error fetching remote snapshots');
             if (this._mounted) this.setState({ entries: OrderedMap(), isBusy: false });
         }
-    }
+    };
 
     upload = async () => {
         this.setState({ isBusy: true });
@@ -269,7 +269,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
             this.setState({ isBusy: false });
             this.refresh();
         }
-    }
+    };
 
 
     fetch = async (e: React.MouseEvent<HTMLElement>) => {
@@ -284,7 +284,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
         } finally {
             if (this._mounted) this.setState({ isBusy: false });
         }
-    }
+    };
 
     remove = async (e: React.MouseEvent<HTMLElement>) => {
         const id = e.currentTarget.getAttribute('data-id');
@@ -298,7 +298,7 @@ export class RemoteStateSnapshots extends PluginUIComponent<
         } catch (e) {
             console.error(e);
         }
-    }
+    };
 
     render() {
         return <>
@@ -345,7 +345,7 @@ class RemoteStateSnapshotList extends PurePluginUIComponent<
         if (qi > 0) url = url.substr(0, qi);
 
         window.open(`${url}?snapshot-url=${encodeURIComponent(entry.url)}`, '_blank');
-    }
+    };
 
     render() {
         return <ul style={{ listStyle: 'none', marginTop: '10px' }} className='msp-state-list'>

+ 11 - 11
src/mol-plugin-ui/state/tree.tsx

@@ -87,7 +87,7 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
         isCollapsed: !!this.props.cell.state.isCollapsed,
         isNull: StateTreeNode.isNull(this.props.cell),
         showLabel: StateTreeNode.showLabel(this.props.cell)
-    }
+    };
 
     static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null {
         const isNull = StateTreeNode.isNull(props.cell);
@@ -187,7 +187,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         isCollapsed: !!this.props.cell.state.isCollapsed,
         action: void 0,
         currentAction: void 0 as StateAction | undefined
-    }
+    };
 
     static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
         const isCurrent = props.cell.parent!.current === props.cell.transform.ref;
@@ -201,46 +201,46 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         e?.preventDefault();
         e?.currentTarget.blur();
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
-    }
+    };
 
     setCurrentRoot = (e?: React.MouseEvent<HTMLElement>) => {
         e?.preventDefault();
         e?.currentTarget.blur();
         PluginCommands.State.SetCurrentObject(this.plugin, { state: this.props.cell.parent!, ref: StateTransform.RootRef });
-    }
+    };
 
     remove = (e?: React.MouseEvent<HTMLElement>) => {
         e?.preventDefault();
         PluginCommands.State.RemoveObject(this.plugin, { state: this.props.cell.parent!, ref: this.ref, removeParentGhosts: true });
-    }
+    };
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
-    }
+    };
 
     toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.State.ToggleExpanded(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
-    }
+    };
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent!, ref: this.ref });
         e.currentTarget.blur();
-    }
+    };
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.Interactivity.ClearHighlights(this.plugin);
         e.currentTarget.blur();
-    }
+    };
 
     hideApply = () => {
         this.setCurrentRoot();
-    }
+    };
 
     get actions() {
         const cell = this.props.cell;
@@ -258,7 +258,7 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
     selectAction: ActionMenu.OnSelect = item => {
         if (!item) return;
         (item?.value as any)();
-    }
+    };
 
     updates(margin: string) {
         const cell = this.props.cell;

+ 13 - 13
src/mol-plugin-ui/structure/components.tsx

@@ -109,12 +109,12 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
         const { structures } = mng.hierarchy.selection;
         if (item.value === null) mng.component.clear(structures);
         else mng.component.applyPreset(structures, item.value as any);
-    }
+    };
 
     undo = () => {
         const task = this.plugin.state.data.undo();
         if (task) this.plugin.runTask(task);
-    }
+    };
 
     render() {
         const undoTitle = this.state.canUndo
@@ -166,9 +166,9 @@ export class AddComponentControls extends PurePluginUIComponent<AddComponentCont
         const structures = this.props.forSelection ? this.currentStructures : this.selectedStructures;
         this.props.onApply();
         this.plugin.managers.structure.component.add(this.state.values, structures);
-    }
+    };
 
-    paramsChanged = (values: any) => this.setState({ values })
+    paramsChanged = (values: any) => this.setState({ values });
 
     render() {
         return <>
@@ -185,7 +185,7 @@ class ComponentOptionsControls extends PurePluginUIComponent<{ isDisabled: boole
         this.subscribe(this.plugin.managers.structure.component.events.optionsUpdated, () => this.forceUpdate());
     }
 
-    update = (options: StructureComponentManager.Options) => this.plugin.managers.structure.component.setOptions(options)
+    update = (options: StructureComponentManager.Options) => this.plugin.managers.structure.component.setOptions(options);
 
     render() {
         return <ParameterControls params={StructureComponentManager.OptionsParams} values={this.plugin.managers.structure.component.state.options} onChangeValues={this.update} isDisabled={this.props.isDisabled} />;
@@ -212,7 +212,7 @@ class ComponentListControls extends PurePluginUIComponent {
 type StructureComponentEntryActions = 'action' | 'label'
 
 class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureComponentRef[] }, { action?: StructureComponentEntryActions }> {
-    state = { action: void 0 as StructureComponentEntryActions | undefined }
+    state = { action: void 0 as StructureComponentEntryActions | undefined };
 
     get pivot() {
         return this.props.group[0];
@@ -228,7 +228,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         e.preventDefault();
         e.currentTarget.blur();
         this.plugin.managers.structure.component.toggleVisibility(this.props.group);
-    }
+    };
 
     get colorByActions() {
         const mng = this.plugin.managers.structure.component;
@@ -282,7 +282,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         if (!item) return;
         this.setState({ action: void 0 });
         (item?.value as any)();
-    }
+    };
 
     remove = () => this.plugin.managers.structure.hierarchy.remove(this.props.group, true);
 
@@ -293,12 +293,12 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
         e.preventDefault();
         if (!this.props.group[0].cell.parent) return;
         PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.group[0].cell.parent!, ref: this.props.group.map(c => c.cell.transform.ref) });
-    }
+    };
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         PluginCommands.Interactivity.ClearHighlights(this.plugin);
-    }
+    };
 
     focus = () => {
         let allHidden = true;
@@ -317,7 +317,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
             if (e.cell.state.isHidden) return;
             return e.cell.obj?.data.boundary.sphere;
         });
-    }
+    };
 
     get reprLabel() {
         // TODO: handle generic reprs.
@@ -329,7 +329,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
 
     private updateLabel = (v: string) => {
         this.plugin.managers.structure.component.updateLabel(this.pivot, v);
-    }
+    };
 
     render() {
         const component = this.pivot;
@@ -370,7 +370,7 @@ class StructureRepresentationEntry extends PurePluginUIComponent<{ group: Struct
         e.preventDefault();
         e.currentTarget.blur();
         this.plugin.managers.structure.component.toggleVisibility(this.props.group, this.props.representation);
-    }
+    };
 
     componentDidMount() {
         this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {

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