Forráskód Böngészése

Merge branch 'master' into molql_integration_PR

Alexander Rose 2 éve
szülő
commit
7d1dc86cfb
33 módosított fájl, 161 hozzáadás és 77 törlés
  1. 4 0
      CHANGELOG.md
  2. 2 0
      src/apps/viewer/app.ts
  3. 2 0
      src/apps/viewer/index.html
  4. 2 0
      src/extensions/geo-export/ui.tsx
  5. 3 1
      src/extensions/mp4-export/controls.ts
  6. 3 1
      src/mol-canvas3d/canvas3d.ts
  7. 1 0
      src/mol-model-formats/structure/common/component.ts
  8. 2 2
      src/mol-model-formats/structure/pdb/atom-site.ts
  9. 3 3
      src/mol-model-formats/structure/pdb/to-cif.ts
  10. 9 8
      src/mol-model-formats/structure/property/bonds/chem_comp.ts
  11. 3 1
      src/mol-model/structure/query/context.ts
  12. 5 2
      src/mol-model/structure/query/queries/filters.ts
  13. 4 2
      src/mol-model/structure/query/queries/generators.ts
  14. 5 2
      src/mol-model/structure/query/queries/modifiers.ts
  15. 3 2
      src/mol-model/structure/structure/unit/bonds/data.ts
  16. 5 4
      src/mol-model/structure/structure/unit/bonds/inter-compute.ts
  17. 14 6
      src/mol-model/structure/structure/unit/bonds/intra-compute.ts
  18. 13 10
      src/mol-plugin-ui/controls/screenshot.tsx
  19. 7 5
      src/mol-plugin-ui/left-panel.tsx
  20. 7 2
      src/mol-plugin-ui/structure/measurements.tsx
  21. 1 1
      src/mol-plugin-ui/viewport/canvas.tsx
  22. 8 5
      src/mol-plugin-ui/viewport/screenshot.tsx
  23. 2 0
      src/mol-plugin-ui/viewport/simple-settings.tsx
  24. 4 2
      src/mol-plugin/behavior/static/state.ts
  25. 1 0
      src/mol-plugin/config.ts
  26. 2 1
      src/mol-plugin/context.ts
  27. 19 4
      src/mol-repr/representation.ts
  28. 4 2
      src/mol-repr/shape/representation.ts
  29. 7 3
      src/mol-repr/structure/complex-representation.ts
  30. 7 3
      src/mol-repr/structure/units-representation.ts
  31. 4 2
      src/mol-repr/volume/representation.ts
  32. 3 2
      src/mol-script/language/symbol-table/structure-query.ts
  33. 2 1
      src/mol-script/runtime/query/table.ts

+ 4 - 0
CHANGELOG.md

@@ -10,6 +10,10 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
 - Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
 - Add ``parentDisplay`` param for interactions representation.
 - Add ``parentDisplay`` param for interactions representation.
 - [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
 - [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
+- Support for ``failIfMajorPerformanceCaveat`` webgl attribute. Add ``PluginConfig.General.AllowMajorPerformanceCaveat`` and ``allow-major-performance-caveat`` Viewer GET param.
+- Fix handling of PDB TER records (#549)
+- Add support for getting multiple loci from a representation (``.getAllLoci()``)
+- Add ``key`` property to intra- and inter-bonds for referencing source data
 
 
 ## [v3.16.0] - 2022-08-25
 ## [v3.16.0] - 2022-08-25
 
 

+ 2 - 0
src/apps/viewer/app.ts

@@ -89,6 +89,7 @@ const DefaultViewerOptions = {
     pickPadding: PluginConfig.General.PickPadding.defaultValue,
     pickPadding: PluginConfig.General.PickPadding.defaultValue,
     enableWboit: PluginConfig.General.EnableWboit.defaultValue,
     enableWboit: PluginConfig.General.EnableWboit.defaultValue,
     preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
     preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
+    allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
 
 
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -159,6 +160,7 @@ export class Viewer {
                 [PluginConfig.General.PickPadding, o.pickPadding],
                 [PluginConfig.General.PickPadding, o.pickPadding],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.General.PreferWebGl1, o.preferWebgl1],
                 [PluginConfig.General.PreferWebGl1, o.preferWebgl1],
+                [PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
                 [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
                 [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],

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

@@ -61,6 +61,7 @@
             var pickPadding = getParam('pick-padding', '[^&]+').trim();
             var pickPadding = getParam('pick-padding', '[^&]+').trim();
             var disableWboit = getParam('disable-wboit', '[^&]+').trim() === '1';
             var disableWboit = getParam('disable-wboit', '[^&]+').trim() === '1';
             var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
             var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
+            var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
 
 
             molstar.Viewer.create('app', {
             molstar.Viewer.create('app', {
                 layoutShowControls: !hideControls,
                 layoutShowControls: !hideControls,
@@ -76,6 +77,7 @@
                 pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
                 pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
                 enableWboit: disableWboit ? false : void 0, // use default value if disable-wboit is not set
                 enableWboit: disableWboit ? false : void 0, // use default value if disable-wboit is not set
                 preferWebgl1: preferWebgl1,
                 preferWebgl1: preferWebgl1,
+                allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
             }).then(viewer => {
             }).then(viewer => {
                 var snapshotId = getParam('snapshot-id', '[^&]+').trim();
                 var snapshotId = getParam('snapshot-id', '[^&]+').trim();
                 if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
                 if (snapshotId) viewer.setRemoteSnapshot(snapshotId);

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

@@ -60,6 +60,8 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
     }
     }
 
 
     componentDidMount() {
     componentDidMount() {
+        if (!this.plugin.canvas3d) return;
+
         const merged = merge(
         const merged = merge(
             this.controls.behaviors.params,
             this.controls.behaviors.params,
             this.plugin.canvas3d!.reprCount
             this.plugin.canvas3d!.reprCount

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

@@ -118,11 +118,13 @@ export class Mp4Controls extends PluginComponent {
     }
     }
 
 
     private init() {
     private init() {
+        if (!this.plugin.canvas3d) return;
+
         this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
         this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
             this.sync();
             this.sync();
         });
         });
 
 
-        this.subscribe(this.plugin.canvas3d?.resized!, () => this.syncInfo());
+        this.subscribe(this.plugin.canvas3d.resized, () => this.syncInfo());
         this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
         this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
 
 
         this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));
         this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));

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

@@ -117,6 +117,7 @@ interface Canvas3DContext {
 
 
 namespace Canvas3DContext {
 namespace Canvas3DContext {
     export const DefaultAttribs = {
     export const DefaultAttribs = {
+        failIfMajorPerformanceCaveat: false,
         /** true by default to avoid issues with Safari (Jan 2021) */
         /** true by default to avoid issues with Safari (Jan 2021) */
         antialias: true,
         antialias: true,
         /** true to support multiple Canvas3D objects with a single context */
         /** true to support multiple Canvas3D objects with a single context */
@@ -132,8 +133,9 @@ namespace Canvas3DContext {
 
 
     export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
     export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
         const a = { ...DefaultAttribs, ...attribs };
         const a = { ...DefaultAttribs, ...attribs };
-        const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
+        const { failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const gl = getGLContext(canvas, {
         const gl = getGLContext(canvas, {
+            failIfMajorPerformanceCaveat,
             antialias,
             antialias,
             preserveDrawingBuffer,
             preserveDrawingBuffer,
             alpha: true, // the renderer requires an alpha channel
             alpha: true, // the renderer requires an alpha channel

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

@@ -32,6 +32,7 @@ const DnaAtomIdsList = [
 /** Used to reduce false positives for atom name-based type guessing */
 /** Used to reduce false positives for atom name-based type guessing */
 const NonPolymerNames = new Set([
 const NonPolymerNames = new Set([
     'FMN', 'NCN', 'FNS', 'FMA', 'ATP', 'ADP', 'AMP', 'GTP', 'GDP', 'GMP', // Mononucleotides
     'FMN', 'NCN', 'FNS', 'FMA', 'ATP', 'ADP', 'AMP', 'GTP', 'GDP', 'GMP', // Mononucleotides
+    'LIG'
 ]);
 ]);
 
 
 const StandardComponents = (function () {
 const StandardComponents = (function () {

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

@@ -39,7 +39,7 @@ export function getAtomSiteTemplate(data: string, count: number) {
     };
     };
 }
 }
 
 
-export function getAtomSite(sites: AtomSiteTemplate, hasTer: boolean): { [K in keyof mmCIF_Schema['atom_site'] | 'partial_charge']?: CifField } {
+export function getAtomSite(sites: AtomSiteTemplate, terIndices: Set<number>): { [K in keyof mmCIF_Schema['atom_site'] | 'partial_charge']?: CifField } {
     const pdbx_PDB_model_num = CifField.ofStrings(sites.pdbx_PDB_model_num);
     const pdbx_PDB_model_num = CifField.ofStrings(sites.pdbx_PDB_model_num);
     const auth_asym_id = CifField.ofTokens(sites.auth_asym_id);
     const auth_asym_id = CifField.ofTokens(sites.auth_asym_id);
     const auth_seq_id = CifField.ofTokens(sites.auth_seq_id);
     const auth_seq_id = CifField.ofTokens(sites.auth_seq_id);
@@ -91,7 +91,7 @@ export function getAtomSite(sites: AtomSiteTemplate, hasTer: boolean): { [K in k
         if (asymIdCounts.has(asymId)) {
         if (asymIdCounts.has(asymId)) {
             // only change the chains name if there are TER records
             // only change the chains name if there are TER records
             // otherwise assume repeated chain name use is from interleaved chains
             // otherwise assume repeated chain name use is from interleaved chains
-            if (hasTer && asymIdChanged) {
+            if (terIndices.has(i)) {
                 const asymIdCount = asymIdCounts.get(asymId)! + 1;
                 const asymIdCount = asymIdCounts.get(asymId)! + 1;
                 asymIdCounts.set(asymId, asymIdCount);
                 asymIdCounts.set(asymId, asymIdCount);
                 currLabelAsymId = `${asymId}_${asymIdCount}`;
                 currLabelAsymId = `${asymId}_${asymIdCount}`;

+ 3 - 3
src/mol-model-formats/structure/pdb/to-cif.ts

@@ -51,7 +51,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
 
 
     let modelNum = 0, modelStr = '';
     let modelNum = 0, modelStr = '';
     let conectRange: [number, number] | undefined = undefined;
     let conectRange: [number, number] | undefined = undefined;
-    let hasTer = false;
+    const terIndices = new Set<number>();
 
 
     for (let i = 0, _i = lines.count; i < _i; i++) {
     for (let i = 0, _i = lines.count; i < _i; i++) {
         let s = indices[2 * i], e = indices[2 * i + 1];
         let s = indices[2 * i], e = indices[2 * i + 1];
@@ -164,7 +164,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
                 break;
                 break;
             case 'T':
             case 'T':
                 if (substringStartsWith(data, s, e, 'TER')) {
                 if (substringStartsWith(data, s, e, 'TER')) {
-                    hasTer = true;
+                    terIndices.add(atomSite.index);
                 }
                 }
         }
         }
     }
     }
@@ -183,7 +183,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
         atomSite.label_entity_id[i] = entityBuilder.getEntityId(compId, moleculeType, asymIds.value(i));
         atomSite.label_entity_id[i] = entityBuilder.getEntityId(compId, moleculeType, asymIds.value(i));
     }
     }
 
 
-    const atom_site = getAtomSite(atomSite, hasTer);
+    const atom_site = getAtomSite(atomSite, terIndices);
     if (!isPdbqt) delete atom_site.partial_charge;
     if (!isPdbqt) delete atom_site.partial_charge;
 
 
     if (conectRange) {
     if (conectRange) {

+ 9 - 8
src/mol-model-formats/structure/property/bonds/chem_comp.ts

@@ -65,7 +65,7 @@ export namespace ComponentBond {
             return e;
             return e;
         }
         }
 
 
-        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount } = data;
+        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount, pdbx_ordinal } = data;
 
 
         let entry = addEntry(comp_id.value(0)!);
         let entry = addEntry(comp_id.value(0)!);
         for (let i = 0; i < _rowCount; i++) {
         for (let i = 0; i < _rowCount; i++) {
@@ -74,6 +74,7 @@ export namespace ComponentBond {
             const nameB = atom_id_2.value(i)!;
             const nameB = atom_id_2.value(i)!;
             const order = value_order.value(i)!;
             const order = value_order.value(i)!;
             const aromatic = pdbx_aromatic_flag.value(i) === 'y';
             const aromatic = pdbx_aromatic_flag.value(i) === 'y';
+            const key = pdbx_ordinal.value(i);
 
 
             if (entry.id !== id) {
             if (entry.id !== id) {
                 entry = addEntry(id);
                 entry = addEntry(id);
@@ -89,29 +90,29 @@ export namespace ComponentBond {
                 case 'quad': ord = 4; break;
                 case 'quad': ord = 4; break;
             }
             }
 
 
-            entry.add(nameA, nameB, ord, flags);
+            entry.add(nameA, nameB, ord, flags, key);
         }
         }
 
 
         return entries;
         return entries;
     }
     }
 
 
     export class Entry {
     export class Entry {
-        readonly map: Map<string, Map<string, { order: number, flags: number }>> = new Map();
+        readonly map: Map<string, Map<string, { order: number, flags: number, key: number }>> = new Map();
 
 
-        add(a: string, b: string, order: number, flags: number, swap = true) {
+        add(a: string, b: string, order: number, flags: number, key: number, swap = true) {
             const e = this.map.get(a);
             const e = this.map.get(a);
             if (e !== void 0) {
             if (e !== void 0) {
                 const f = e.get(b);
                 const f = e.get(b);
                 if (f === void 0) {
                 if (f === void 0) {
-                    e.set(b, { order, flags });
+                    e.set(b, { order, flags, key });
                 }
                 }
             } else {
             } else {
-                const map = new Map<string, { order: number, flags: number }>();
-                map.set(b, { order, flags });
+                const map = new Map<string, { order: number, flags: number, key: number }>();
+                map.set(b, { order, flags, key });
                 this.map.set(a, map);
                 this.map.set(a, map);
             }
             }
 
 
-            if (swap) this.add(b, a, order, flags, false);
+            if (swap) this.add(b, a, order, flags, key, false);
         }
         }
 
 
         constructor(public readonly id: string) { }
         constructor(public readonly id: string) { }

+ 3 - 1
src/mol-model/structure/query/context.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import { Structure, StructureElement, Unit } from '../structure';
 import { Structure, StructureElement, Unit } from '../structure';
@@ -113,6 +114,7 @@ class QueryContextBondInfo<U extends Unit = Unit> {
     bIndex: StructureElement.UnitIndex = 0 as StructureElement.UnitIndex;
     bIndex: StructureElement.UnitIndex = 0 as StructureElement.UnitIndex;
     type: BondType = BondType.Flag.None;
     type: BondType = BondType.Flag.None;
     order: number = 0;
     order: number = 0;
+    key: number = -1;
 
 
     private testFn: QueryPredicate = defaultBondTest;
     private testFn: QueryPredicate = defaultBondTest;
 
 

+ 5 - 2
src/mol-model/structure/query/queries/filters.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import { SetUtils } from '../../../../mol-util/set';
 import { SetUtils } from '../../../../mol-util/set';
@@ -246,7 +247,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
 
 
         const inputUnit = input.unitMap.get(unit.id) as Unit.Atomic;
         const inputUnit = input.unitMap.get(unit.id) as Unit.Atomic;
 
 
-        const { offset, b, edgeProps: { flags, order } } = inputUnit.bonds;
+        const { offset, b, edgeProps: { flags, order, key } } = inputUnit.bonds;
         const bondedUnits = interBonds.getConnectedUnits(unit.id);
         const bondedUnits = interBonds.getConnectedUnits(unit.id);
         const buCount = bondedUnits.length;
         const buCount = bondedUnits.length;
 
 
@@ -271,6 +272,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
                 atomicBond.bIndex = b[l] as StructureElement.UnitIndex;
                 atomicBond.bIndex = b[l] as StructureElement.UnitIndex;
                 atomicBond.type = flags[l];
                 atomicBond.type = flags[l];
                 atomicBond.order = order[l];
                 atomicBond.order = order[l];
+                atomicBond.key = key[l];
                 if (atomicBond.test(queryCtx, true)) return true;
                 if (atomicBond.test(queryCtx, true)) return true;
             }
             }
 
 
@@ -293,6 +295,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
                     atomicBond.bIndex = bond.indexB;
                     atomicBond.bIndex = bond.indexB;
                     atomicBond.type = bond.props.flag;
                     atomicBond.type = bond.props.flag;
                     atomicBond.order = bond.props.order;
                     atomicBond.order = bond.props.order;
+                    atomicBond.key = bond.props.key;
                     if (atomicBond.test(queryCtx, true)) return true;
                     if (atomicBond.test(queryCtx, true)) return true;
                 }
                 }
             }
             }

+ 4 - 2
src/mol-model/structure/query/queries/generators.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -322,7 +322,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
         for (const unit of structure.units) {
         for (const unit of structure.units) {
             if (unit.kind !== Unit.Kind.Atomic) continue;
             if (unit.kind !== Unit.Kind.Atomic) continue;
 
 
-            const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order } } = unit.bonds;
+            const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order, key } } = unit.bonds;
             atomicBond.a.unit = unit;
             atomicBond.a.unit = unit;
             atomicBond.b.unit = unit;
             atomicBond.b.unit = unit;
             for (let i = 0 as StructureElement.UnitIndex, _i = unit.elements.length; i < _i; i++) {
             for (let i = 0 as StructureElement.UnitIndex, _i = unit.elements.length; i < _i; i++) {
@@ -335,6 +335,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
                     atomicBond.b.element = unit.elements[intraBondB[lI]];
                     atomicBond.b.element = unit.elements[intraBondB[lI]];
                     atomicBond.type = flags[lI];
                     atomicBond.type = flags[lI];
                     atomicBond.order = order[lI];
                     atomicBond.order = order[lI];
+                    atomicBond.key = key[lI];
                     // No need to "swap test" because each bond direction will be visited eventually.
                     // No need to "swap test" because each bond direction will be visited eventually.
                     if (atomicBond.test(ctx, false)) {
                     if (atomicBond.test(ctx, false)) {
                         const b = structure.subsetBuilder(false);
                         const b = structure.subsetBuilder(false);
@@ -358,6 +359,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
             atomicBond.bIndex = bond.indexB;
             atomicBond.bIndex = bond.indexB;
             atomicBond.order = bond.props.order;
             atomicBond.order = bond.props.order;
             atomicBond.type = bond.props.flag;
             atomicBond.type = bond.props.flag;
+            atomicBond.key = bond.props.key;
 
 
             // No need to "swap test" because each bond direction will be visited eventually.
             // No need to "swap test" because each bond direction will be visited eventually.
             if (atomicBond.test(ctx, false)) {
             if (atomicBond.test(ctx, false)) {

+ 5 - 2
src/mol-model/structure/query/queries/modifiers.ts

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import { Segmentation, SortedArray } from '../../../../mol-data/int';
 import { Segmentation, SortedArray } from '../../../../mol-data/int';
@@ -370,7 +371,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
         }
         }
 
 
         const inputUnitA = inputStructure.unitMap.get(unit.id) as Unit.Atomic;
         const inputUnitA = inputStructure.unitMap.get(unit.id) as Unit.Atomic;
-        const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order } } = inputUnitA.bonds;
+        const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order, key } } = inputUnitA.bonds;
 
 
         atomicBond.setStructure(inputStructure);
         atomicBond.setStructure(inputStructure);
 
 
@@ -397,6 +398,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
                 atomicBond.b.element = bElement;
                 atomicBond.b.element = bElement;
                 atomicBond.type = flags[lI];
                 atomicBond.type = flags[lI];
                 atomicBond.order = order[lI];
                 atomicBond.order = order[lI];
+                atomicBond.key = key[lI];
 
 
                 if (atomicBond.test(ctx, true)) {
                 if (atomicBond.test(ctx, true)) {
                     builder.addToUnit(unit.id, bElement);
                     builder.addToUnit(unit.id, bElement);
@@ -427,6 +429,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
                     atomicBond.b.element = bElement;
                     atomicBond.b.element = bElement;
                     atomicBond.type = bond.props.flag;
                     atomicBond.type = bond.props.flag;
                     atomicBond.order = bond.props.order;
                     atomicBond.order = bond.props.order;
+                    atomicBond.key = bond.props.key;
 
 
                     if (atomicBond.test(ctx, true)) {
                     if (atomicBond.test(ctx, true)) {
                         builder.addToUnit(bondedUnit.unitB, bElement);
                         builder.addToUnit(bondedUnit.unitB, bElement);

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

@@ -15,16 +15,17 @@ import { InterUnitGraph } from '../../../../../mol-math/graph/inter-unit-graph';
 type IntraUnitBonds = IntAdjacencyGraph<StructureElement.UnitIndex, {
 type IntraUnitBonds = IntAdjacencyGraph<StructureElement.UnitIndex, {
     readonly order: ArrayLike<number>,
     readonly order: ArrayLike<number>,
     readonly flags: ArrayLike<BondType.Flag>
     readonly flags: ArrayLike<BondType.Flag>
+    readonly key: ArrayLike<number>,
 }, {
 }, {
     /** can remap even with dynamicBonds on, e.g., for water molecules */
     /** can remap even with dynamicBonds on, e.g., for water molecules */
     readonly canRemap?: boolean
     readonly canRemap?: boolean
 }>
 }>
 
 
 namespace IntraUnitBonds {
 namespace IntraUnitBonds {
-    export const Empty: IntraUnitBonds = IntAdjacencyGraph.create([], [], [], 0, { flags: [], order: [] });
+    export const Empty: IntraUnitBonds = IntAdjacencyGraph.create([], [], [], 0, { flags: [], order: [], key: [] });
 }
 }
 
 
-type InterUnitEdgeProps = { readonly order: number, readonly flag: BondType.Flag }
+type InterUnitEdgeProps = { readonly order: number, readonly flag: BondType.Flag, readonly key: number }
 
 
 class InterUnitBonds extends InterUnitGraph<number, StructureElement.UnitIndex, InterUnitEdgeProps> {
 class InterUnitBonds extends InterUnitGraph<number, StructureElement.UnitIndex, InterUnitEdgeProps> {
     /** Get inter-unit bond given a bond-location */
     /** Get inter-unit bond given a bond-location */

+ 5 - 4
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -80,7 +80,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 
 
         if (!props.forceCompute && indexPairs) {
         if (!props.forceCompute && indexPairs) {
             const { maxDistance } = indexPairs;
             const { maxDistance } = indexPairs;
-            const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
+            const { offset, b, edgeProps: { order, distance, flag, key } } = indexPairs.bonds;
 
 
             const srcA = sourceIndex.value(aI);
             const srcA = sourceIndex.value(aI);
             const aeI = getElementIdx(type_symbolA.value(aI));
             const aeI = getElementIdx(type_symbolA.value(aI));
@@ -113,7 +113,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 }
                 }
 
 
                 if (add) {
                 if (add) {
-                    builder.add(_aI, _bI, { order: order[i], flag: flag[i] });
+                    builder.add(_aI, _bI, { order: order[i], flag: flag[i], key: key[i] });
                 }
                 }
             }
             }
             continue; // assume `indexPairs` supplies all bonds
             continue; // assume `indexPairs` supplies all bonds
@@ -131,7 +131,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 // check if the bond is within MAX_RADIUS for this pair of units
                 // check if the bond is within MAX_RADIUS for this pair of units
                 if (getDistance(unitA, aI, unitB, p.atomIndex) > maxRadius) continue;
                 if (getDistance(unitA, aI, unitB, p.atomIndex) > maxRadius) continue;
 
 
-                builder.add(_aI, _bI, { order: se.order, flag: se.flags });
+                builder.add(_aI, _bI, { order: se.order, flag: se.flags, key: se.rowIndex });
                 added = true;
                 added = true;
             }
             }
             // assume, for an atom, that if any inter unit bond is given
             // assume, for an atom, that if any inter unit bond is given
@@ -187,7 +187,8 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 const compIdB = label_comp_idB.value(residueIndexB[bI]);
                 const compIdB = label_comp_idB.value(residueIndexB[bI]);
                 builder.add(_aI, _bI, {
                 builder.add(_aI, _bI, {
                     order: getInterBondOrderFromTable(compIdA, compIdB, atomIdA, atomIdB),
                     order: getInterBondOrderFromTable(compIdA, compIdB, atomIdA, atomIdB),
-                    flag: (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed
+                    flag: (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed,
+                    key: -1
                 });
                 });
             }
             }
         }
         }

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

@@ -24,17 +24,19 @@ import { Model } from '../../../model/model';
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const v3distance = Vec3.distance;
 const v3distance = Vec3.distance;
 
 
-function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
+function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], _key: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const flags = new Uint16Array(builder.slotCount);
     const flags = new Uint16Array(builder.slotCount);
     const order = new Int8Array(builder.slotCount);
     const order = new Int8Array(builder.slotCount);
+    const key = new Uint32Array(builder.slotCount);
     for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
     for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
         builder.addNextEdge();
         builder.addNextEdge();
         builder.assignProperty(flags, _flags[i]);
         builder.assignProperty(flags, _flags[i]);
         builder.assignProperty(order, _order[i]);
         builder.assignProperty(order, _order[i]);
+        builder.assignProperty(key, _key[i]);
     }
     }
 
 
-    return builder.createGraph({ flags, order }, { canRemap });
+    return builder.createGraph({ flags, order, key }, { canRemap });
 }
 }
 
 
 const tmpDistVecA = Vec3();
 const tmpDistVecA = Vec3();
@@ -53,7 +55,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const { type_symbol } = unit.model.atomicHierarchy.atoms;
     const { type_symbol } = unit.model.atomicHierarchy.atoms;
     const atomCount = unit.elements.length;
     const atomCount = unit.elements.length;
     const { maxDistance } = indexPairs;
     const { maxDistance } = indexPairs;
-    const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
+    const { offset, b, edgeProps: { order, distance, flag, key } } = indexPairs.bonds;
 
 
     const { atomSourceIndex: sourceIndex } = unit.model.atomicHierarchy;
     const { atomSourceIndex: sourceIndex } = unit.model.atomicHierarchy;
     const { invertedIndex } = Model.getInvertedAtomSourceIndex(unit.model);
     const { invertedIndex } = Model.getInvertedAtomSourceIndex(unit.model);
@@ -62,6 +64,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const atomB: StructureElement.UnitIndex[] = [];
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
     const flags: number[] = [];
     const orders: number[] = [];
     const orders: number[] = [];
+    const keys: number[] = [];
 
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI = atoms[_aI];
         const aI = atoms[_aI];
@@ -104,11 +107,12 @@ function findIndexPairBonds(unit: Unit.Atomic) {
                 atomB[atomB.length] = _bI;
                 atomB[atomB.length] = _bI;
                 orders[orders.length] = order[i];
                 orders[orders.length] = order[i];
                 flags[flags.length] = flag[i];
                 flags[flags.length] = flag[i];
+                keys[keys.length] = key[i];
             }
             }
         }
         }
     }
     }
 
 
-    return getGraph(atomA, atomB, orders, flags, atomCount, false);
+    return getGraph(atomA, atomB, orders, flags, keys, atomCount, false);
 }
 }
 
 
 function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
 function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
@@ -132,9 +136,10 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
     const atomB: StructureElement.UnitIndex[] = [];
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
     const flags: number[] = [];
     const order: number[] = [];
     const order: number[] = [];
+    const key: number[] = [];
 
 
     let lastResidue = -1;
     let lastResidue = -1;
-    let componentMap: Map<string, Map<string, { flags: number, order: number }>> | undefined = void 0;
+    let componentMap: Map<string, Map<string, { flags: number, order: number, key: number }>> | undefined = void 0;
 
 
     let isWatery = true, isDictionaryBased = true, isSequenced = true;
     let isWatery = true, isDictionaryBased = true, isSequenced = true;
 
 
@@ -162,6 +167,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                 atomB[atomB.length] = _bI;
                 atomB[atomB.length] = _bI;
                 flags[flags.length] = se.flags;
                 flags[flags.length] = se.flags;
                 order[order.length] = se.order;
                 order[order.length] = se.order;
+                key[key.length] = se.rowIndex;
 
 
                 if (!hasStructConn) structConnAdded.clear();
                 if (!hasStructConn) structConnAdded.clear();
                 hasStructConn = true;
                 hasStructConn = true;
@@ -230,6 +236,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                         flag |= BondType.Flag.MetallicCoordination;
                         flag |= BondType.Flag.MetallicCoordination;
                     }
                     }
                     flags[flags.length] = flag;
                     flags[flags.length] = flag;
+                    key[key.length] = e.key;
                 }
                 }
                 continue;
                 continue;
             }
             }
@@ -243,6 +250,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                 atomB[atomB.length] = _bI;
                 atomB[atomB.length] = _bI;
                 order[order.length] = getIntraBondOrderFromTable(compId, atomIdA, label_atom_id.value(bI));
                 order[order.length] = getIntraBondOrderFromTable(compId, atomIdA, label_atom_id.value(bI));
                 flags[flags.length] = (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed;
                 flags[flags.length] = (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed;
+                key[key.length] = -1;
 
 
                 const seqIdB = label_seq_id.value(rbI);
                 const seqIdB = label_seq_id.value(rbI);
 
 
@@ -253,7 +261,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
     }
     }
 
 
     const canRemap = isWatery || (isDictionaryBased && isSequenced);
     const canRemap = isWatery || (isDictionaryBased && isSequenced);
-    return getGraph(atomA, atomB, order, flags, atomCount, canRemap);
+    return getGraph(atomA, atomB, order, flags, key, atomCount, canRemap);
 }
 }
 
 
 function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputationProps>) {
 function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputationProps>) {

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

@@ -1,7 +1,8 @@
 /**
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
 
 
 import * as React from 'react';
 import * as React from 'react';
@@ -25,7 +26,7 @@ export interface ScreenshotPreviewProps {
 const _ScreenshotPreview = (props: ScreenshotPreviewProps) => {
 const _ScreenshotPreview = (props: ScreenshotPreviewProps) => {
     const { plugin, cropFrameColor } = props;
     const { plugin, cropFrameColor } = props;
 
 
-    const helper = plugin.helpers.viewportScreenshot!;
+    const helper = plugin.helpers.viewportScreenshot;
     const [currentCanvas, setCurrentCanvas] = useState<HTMLCanvasElement | null>(null);
     const [currentCanvas, setCurrentCanvas] = useState<HTMLCanvasElement | null>(null);
     const canvasRef = useRef<HTMLCanvasElement | null>(null);
     const canvasRef = useRef<HTMLCanvasElement | null>(null);
     const propsRef = useRef(props);
     const propsRef = useRef(props);
@@ -70,8 +71,8 @@ const _ScreenshotPreview = (props: ScreenshotPreviewProps) => {
         subscribe(plugin.state.data.behaviors.isUpdating, v => {
         subscribe(plugin.state.data.behaviors.isUpdating, v => {
             if (!v) isDirty = true;
             if (!v) isDirty = true;
         });
         });
-        subscribe(helper.behaviors.values, () => isDirty = true);
-        subscribe(helper.behaviors.cropParams, () => isDirty = true);
+        subscribe(helper?.behaviors.values, () => isDirty = true);
+        subscribe(helper?.behaviors.cropParams, () => isDirty = true);
 
 
         let resizeObserver: any = void 0;
         let resizeObserver: any = void 0;
         if (typeof ResizeObserver !== 'undefined') {
         if (typeof ResizeObserver !== 'undefined') {
@@ -108,7 +109,9 @@ export const ScreenshotPreview = React.memo(_ScreenshotPreview, (prev, next) =>
 
 
 declare const ResizeObserver: any;
 declare const ResizeObserver: any;
 
 
-function drawPreview(helper: ViewportScreenshotHelper, target: HTMLCanvasElement, customBackground?: string, borderColor?: string, borderWidth?: number) {
+function drawPreview(helper: ViewportScreenshotHelper | undefined, target: HTMLCanvasElement, customBackground?: string, borderColor?: string, borderWidth?: number) {
+    if (!helper) return;
+
     const { canvas, width, height } = helper.getPreview()!;
     const { canvas, width, height } = helper.getPreview()!;
     const ctx = target.getContext('2d');
     const ctx = target.getContext('2d');
     if (!ctx) return;
     if (!ctx) return;
@@ -151,9 +154,9 @@ function drawPreview(helper: ViewportScreenshotHelper, target: HTMLCanvasElement
 
 
 function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: { plugin: PluginContext, canvas: HTMLCanvasElement | null, color?: string }) {
 function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: { plugin: PluginContext, canvas: HTMLCanvasElement | null, color?: string }) {
     const helper = plugin.helpers.viewportScreenshot;
     const helper = plugin.helpers.viewportScreenshot;
-    const params = useBehavior(helper?.behaviors.values!);
-    const cropParams = useBehavior(helper?.behaviors.cropParams!);
-    const crop = useBehavior(helper?.behaviors.relativeCrop!);
+    const params = useBehavior(helper?.behaviors.values);
+    const cropParams = useBehavior(helper?.behaviors.cropParams);
+    const crop = useBehavior(helper?.behaviors.relativeCrop);
     const cropFrameRef = useRef<Viewport>({ x: 0, y: 0, width: 0, height: 0 });
     const cropFrameRef = useRef<Viewport>({ x: 0, y: 0, width: 0, height: 0 });
     useBehavior(params?.resolution.name === 'viewport' ? plugin.canvas3d?.resized : void 0);
     useBehavior(params?.resolution.name === 'viewport' ? plugin.canvas3d?.resized : void 0);
 
 
@@ -161,7 +164,7 @@ function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: {
     const [start, setStart] = useState([0, 0]);
     const [start, setStart] = useState([0, 0]);
     const [current, setCurrent] = useState([0, 0]);
     const [current, setCurrent] = useState([0, 0]);
 
 
-    if (!helper || !canvas) return null;
+    if (!helper || !canvas || !crop) return null;
 
 
     const { width, height } = helper.getSizeAndViewport();
     const { width, height } = helper.getSizeAndViewport();
 
 
@@ -267,7 +270,7 @@ function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: {
 
 
     function finish() {
     function finish() {
         const cropFrame = cropFrameRef.current;
         const cropFrame = cropFrameRef.current;
-        if (cropParams.auto) {
+        if (cropParams?.auto) {
             helper?.behaviors.cropParams.next({ ...cropParams, auto: false });
             helper?.behaviors.cropParams.next({ ...cropParams, auto: false });
         }
         }
         helper?.behaviors.relativeCrop.next({
         helper?.behaviors.relativeCrop.next({

+ 7 - 5
src/mol-plugin-ui/left-panel.tsx

@@ -141,11 +141,13 @@ class FullSettings extends PluginUIComponent {
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
         this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
 
 
-        this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
-            if (state.radiusMax !== undefined || state.radius !== undefined) {
-                this.forceUpdate();
-            }
-        });
+        if (this.plugin.canvas3d) {
+            this.subscribe(this.plugin.canvas3d.camera.stateChanged, state => {
+                if (state.radiusMax !== undefined || state.radius !== undefined) {
+                    this.forceUpdate();
+                }
+            });
+        }
     }
     }
 
 
     render() {
     render() {

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

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -288,7 +288,12 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         for (const loci of this.lociArray) {
         for (const loci of this.lociArray) {
             this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
             this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
         }
         }
-        this.plugin.managers.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
+        const reprLocis = this.props.cell.obj?.data.repr.getAllLoci();
+        if (reprLocis) {
+            for (const loci of reprLocis) {
+                this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
+            }
+        }
     };
     };
 
 
     clearHighlight = () => {
     clearHighlight = () => {

+ 1 - 1
src/mol-plugin-ui/viewport/canvas.tsx

@@ -59,7 +59,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
         return <div className='msp-no-webgl'>
         return <div className='msp-no-webgl'>
             <div>
             <div>
                 <p><b>WebGL does not seem to be available.</b></p>
                 <p><b>WebGL does not seem to be available.</b></p>
-                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p>
+                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps. Also, make sure hardware acceleration is enabled in your browser.</p>
                 <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
                 <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
             </div>
             </div>
         </div>;
         </div>;

+ 8 - 5
src/mol-plugin-ui/viewport/screenshot.tsx

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -96,18 +96,21 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
 }
 }
 
 
 function ScreenshotParams({ plugin, isDisabled }: { plugin: PluginContext, isDisabled: boolean }) {
 function ScreenshotParams({ plugin, isDisabled }: { plugin: PluginContext, isDisabled: boolean }) {
-    const helper = plugin.helpers.viewportScreenshot!;
-    const values = useBehavior(helper.behaviors.values);
+    const helper = plugin.helpers.viewportScreenshot;
+
+    const values = useBehavior(helper?.behaviors.values);
+    if (!helper) return null;
 
 
     return <ParameterControls params={helper.params} values={values} onChangeValues={v => helper.behaviors.values.next(v)} isDisabled={isDisabled} />;
     return <ParameterControls params={helper.params} values={values} onChangeValues={v => helper.behaviors.values.next(v)} isDisabled={isDisabled} />;
 }
 }
 
 
 function CropControls({ plugin }: { plugin: PluginContext }) {
 function CropControls({ plugin }: { plugin: PluginContext }) {
     const helper = plugin.helpers.viewportScreenshot;
     const helper = plugin.helpers.viewportScreenshot;
-    const cropParams = useBehavior(helper?.behaviors.cropParams!);
+
+    const cropParams = useBehavior(helper?.behaviors.cropParams);
     useBehavior(helper?.behaviors.relativeCrop);
     useBehavior(helper?.behaviors.relativeCrop);
 
 
-    if (!helper) return null;
+    if (!helper || !cropParams) return null;
 
 
     return <div style={{ width: '100%', height: '24px', marginTop: '8px' }}>
     return <div style={{ width: '100%', height: '24px', marginTop: '8px' }}>
         <ToggleButton icon={CropOrginalSvg} title='Auto-crop' inline isSelected={cropParams.auto}
         <ToggleButton icon={CropOrginalSvg} title='Auto-crop' inline isSelected={cropParams.auto}

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

@@ -22,6 +22,8 @@ import { ViewportHelpContent } from './help';
 
 
 export class SimpleSettingsControl extends PluginUIComponent {
 export class SimpleSettingsControl extends PluginUIComponent {
     componentDidMount() {
     componentDidMount() {
+        if (!this.plugin.canvas3d) return;
+
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
 
 
         this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
         this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {

+ 4 - 2
src/mol-plugin/behavior/static/state.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -119,7 +119,9 @@ export function Highlight(ctx: PluginContext) {
                 ctx.managers.interactivity.lociHighlights.highlight({ loci: Structure.Loci(cell.obj.data) }, false);
                 ctx.managers.interactivity.lociHighlights.highlight({ loci: Structure.Loci(cell.obj.data) }, false);
             } else if (cell && SO.isRepresentation3D(cell.obj)) {
             } else if (cell && SO.isRepresentation3D(cell.obj)) {
                 const { repr } = cell.obj.data;
                 const { repr } = cell.obj.data;
-                ctx.managers.interactivity.lociHighlights.highlight({ loci: repr.getLoci(), repr }, false);
+                for (const loci of repr.getAllLoci()) {
+                    ctx.managers.interactivity.lociHighlights.highlight({ loci, repr }, false);
+                }
             } else if (SO.Molecule.Structure.Selections.is(cell.obj)) {
             } else if (SO.Molecule.Structure.Selections.is(cell.obj)) {
                 for (const entry of cell.obj.data) {
                 for (const entry of cell.obj.data) {
                     ctx.managers.interactivity.lociHighlights.highlight({ loci: entry.loci }, false);
                     ctx.managers.interactivity.lociHighlights.highlight({ loci: entry.loci }, false);

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

@@ -35,6 +35,7 @@ export const PluginConfig = {
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // TODO: check back in a few weeks to see if it was fixed
         // TODO: check back in a few weeks to see if it was fixed
         PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
         PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
+        AllowMajorPerformanceCaveat: item('plugin-config.allow-major-performance-caveat', false),
     },
     },
     State: {
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

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

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

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

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -154,8 +154,8 @@ interface Representation<D, P extends PD.Params = {}, S extends Representation.S
     createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
     createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
     setState: (state: Partial<S>) => void
     setState: (state: Partial<S>) => void
     setTheme: (theme: Theme) => void
     setTheme: (theme: Theme) => void
-    /** If no pickingId is given, returns a Loci for the whole representation */
-    getLoci: (pickingId?: PickingId) => ModelLoci
+    getLoci: (pickingId: PickingId) => ModelLoci
+    getAllLoci: () => ModelLoci[]
     mark: (loci: ModelLoci, action: MarkerAction) => boolean
     mark: (loci: ModelLoci, action: MarkerAction) => boolean
     destroy: () => void
     destroy: () => void
 }
 }
@@ -227,6 +227,7 @@ namespace Representation {
         setState: () => {},
         setState: () => {},
         setTheme: () => {},
         setTheme: () => {},
         getLoci: () => EmptyLoci,
         getLoci: () => EmptyLoci,
+        getAllLoci: () => [],
         mark: () => false,
         mark: () => false,
         destroy: () => {}
         destroy: () => {}
     };
     };
@@ -327,7 +328,7 @@ namespace Representation {
             },
             },
             get state() { return currentState; },
             get state() { return currentState; },
             get theme() { return currentTheme; },
             get theme() { return currentTheme; },
-            getLoci: (pickingId?: PickingId) => {
+            getLoci: (pickingId: PickingId) => {
                 const { visuals } = currentProps;
                 const { visuals } = currentProps;
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     if (!visuals || visuals.includes(reprMap[i])) {
                     if (!visuals || visuals.includes(reprMap[i])) {
@@ -337,6 +338,16 @@ namespace Representation {
                 }
                 }
                 return EmptyLoci;
                 return EmptyLoci;
             },
             },
+            getAllLoci: () => {
+                const loci: ModelLoci[] = [];
+                const { visuals } = currentProps;
+                for (let i = 0, il = reprList.length; i < il; ++i) {
+                    if (!visuals || visuals.includes(reprMap[i])) {
+                        loci.push(...reprList[i].getAllLoci());
+                    }
+                }
+                return loci;
+            },
             mark: (loci: ModelLoci, action: MarkerAction) => {
             mark: (loci: ModelLoci, action: MarkerAction) => {
                 let marked = false;
                 let marked = false;
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
@@ -399,6 +410,10 @@ namespace Representation {
                 // TODO
                 // TODO
                 return EmptyLoci;
                 return EmptyLoci;
             },
             },
+            getAllLoci: () => {
+                // TODO
+                return [];
+            },
             mark: (loci: ModelLoci, action: MarkerAction) => {
             mark: (loci: ModelLoci, action: MarkerAction) => {
                 // TODO
                 // TODO
                 return false;
                 return false;

+ 4 - 2
src/mol-repr/shape/representation.ts

@@ -213,14 +213,16 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
         get geometryVersion() { return geometryVersion; },
         get geometryVersion() { return geometryVersion; },
         updated,
         updated,
         createOrUpdate,
         createOrUpdate,
-        getLoci(pickingId?: PickingId) {
-            if (pickingId === undefined) return Shape.Loci(_shape);
+        getLoci(pickingId: PickingId) {
             const { objectId, groupId, instanceId } = pickingId;
             const { objectId, groupId, instanceId } = pickingId;
             if (_renderObject && _renderObject.id === objectId) {
             if (_renderObject && _renderObject.id === objectId) {
                 return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId), instance: instanceId }]);
                 return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId), instance: instanceId }]);
             }
             }
             return EmptyLoci;
             return EmptyLoci;
         },
         },
+        getAllLoci() {
+            return [Shape.Loci(_shape)];
+        },
         mark(loci: Loci, action: MarkerAction) {
         mark(loci: Loci, action: MarkerAction) {
             if (!MarkerActions.is(_state.markerActions, action)) return false;
             if (!MarkerActions.is(_state.markerActions, action)) return false;
             if (ShapeGroup.isLoci(loci) || Shape.isLoci(loci)) {
             if (ShapeGroup.isLoci(loci) || Shape.isLoci(loci)) {

+ 7 - 3
src/mol-repr/structure/complex-representation.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -72,11 +72,14 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         });
         });
     }
     }
 
 
-    function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure.target);
+    function getLoci(pickingId: PickingId) {
         return visual ? visual.getLoci(pickingId) : EmptyLoci;
         return visual ? visual.getLoci(pickingId) : EmptyLoci;
     }
     }
 
 
+    function getAllLoci() {
+        return [Structure.Loci(_structure.target)];
+    }
+
     function mark(loci: Loci, action: MarkerAction) {
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false;
         if (!_structure) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
@@ -157,6 +160,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         setState,
         setState,
         setTheme,
         setTheme,
         getLoci,
         getLoci,
+        getAllLoci,
         mark,
         mark,
         destroy
         destroy
     };
     };

+ 7 - 3
src/mol-repr/structure/units-representation.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -185,8 +185,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         });
         });
     }
     }
 
 
-    function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure.target);
+    function getLoci(pickingId: PickingId) {
         let loci: Loci = EmptyLoci;
         let loci: Loci = EmptyLoci;
         visuals.forEach(({ visual }) => {
         visuals.forEach(({ visual }) => {
             const _loci = visual.getLoci(pickingId);
             const _loci = visual.getLoci(pickingId);
@@ -195,6 +194,10 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         return loci;
         return loci;
     }
     }
 
 
+    function getAllLoci() {
+        return [Structure.Loci(_structure.target)];
+    }
+
     function mark(loci: Loci, action: MarkerAction) {
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false;
         if (!_structure) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
@@ -302,6 +305,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         setState,
         setState,
         setTheme,
         setTheme,
         getLoci,
         getLoci,
+        getAllLoci,
         mark,
         mark,
         destroy
         destroy
     };
     };

+ 4 - 2
src/mol-repr/volume/representation.ts

@@ -358,10 +358,12 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         createOrUpdate,
         createOrUpdate,
         setState,
         setState,
         setTheme,
         setTheme,
-        getLoci: (pickingId?: PickingId): Loci => {
-            if (pickingId === undefined) return getLoci(_volume, _props);
+        getLoci: (pickingId: PickingId): Loci => {
             return visual ? visual.getLoci(pickingId) : EmptyLoci;
             return visual ? visual.getLoci(pickingId) : EmptyLoci;
         },
         },
+        getAllLoci: (): Loci[] => {
+            return [getLoci(_volume, _props)];
+        },
         mark,
         mark,
         destroy
         destroy
     };
     };

+ 3 - 2
src/mol-script/language/symbol-table/structure-query.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -333,6 +333,7 @@ const bondProperty = {
 
 
     flags: bondProp(Types.BondFlags),
     flags: bondProp(Types.BondFlags),
     order: bondProp(Type.Num),
     order: bondProp(Type.Num),
+    key: bondProp(Type.Num),
     length: bondProp(Type.Num),
     length: bondProp(Type.Num),
     atomA: bondProp(Types.ElementReference),
     atomA: bondProp(Types.ElementReference),
     atomB: bondProp(Types.ElementReference)
     atomB: bondProp(Types.ElementReference)
@@ -356,5 +357,5 @@ export const structureQuery = {
     combinator,
     combinator,
     atomSet,
     atomSet,
     atomProperty,
     atomProperty,
-    bondProperty: bondProperty
+    bondProperty
 };
 };

+ 2 - 1
src/mol-script/runtime/query/table.ts

@@ -1,5 +1,5 @@
 /**
 /**
- * Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -377,6 +377,7 @@ const symbols = [
     // ============= BOND PROPERTIES ================
     // ============= BOND PROPERTIES ================
     D(MolScript.structureQuery.bondProperty.order, (ctx, xs) => ctx.atomicBond.order),
     D(MolScript.structureQuery.bondProperty.order, (ctx, xs) => ctx.atomicBond.order),
     D(MolScript.structureQuery.bondProperty.flags, (ctx, xs) => ctx.atomicBond.type),
     D(MolScript.structureQuery.bondProperty.flags, (ctx, xs) => ctx.atomicBond.type),
+    D(MolScript.structureQuery.bondProperty.key, (ctx, xs) => ctx.atomicBond.key),
     D(MolScript.structureQuery.bondProperty.atomA, (ctx, xs) => ctx.atomicBond.a),
     D(MolScript.structureQuery.bondProperty.atomA, (ctx, xs) => ctx.atomicBond.a),
     D(MolScript.structureQuery.bondProperty.atomB, (ctx, xs) => ctx.atomicBond.b),
     D(MolScript.structureQuery.bondProperty.atomB, (ctx, xs) => ctx.atomicBond.b),
     D(MolScript.structureQuery.bondProperty.length, (ctx, xs) => ctx.atomicBond.length),
     D(MolScript.structureQuery.bondProperty.length, (ctx, xs) => ctx.atomicBond.length),