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

Merge branch 'master' of https://github.com/molstar/molstar into pr/giagitom/533

Alexander Rose 2 éve
szülő
commit
8589777bac

+ 7 - 1
CHANGELOG.md

@@ -6,9 +6,15 @@ Note that since we don't clearly distinguish between a public and private interf
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
+- [Fix] Clone ``Canvas3DParams`` when creating a ``Canvas3D`` instance to prevent shared state between multiple instances
+- Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
+- Add ``parentDisplay`` param for interactions representation.
+- Integration of Dual depth peeling - OIT method
+
+## [v3.16.0] - 2022-08-25
+
 - Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
 - Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
 - Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
 - Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
-- Integration of Dual depth peeling - OIT method
 
 
 ## [v3.15.0] - 2022-08-23
 ## [v3.15.0] - 2022-08-23
 
 

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "molstar",
   "name": "molstar",
-  "version": "3.15.0",
+  "version": "3.16.0",
   "lockfileVersion": 2,
   "lockfileVersion": 2,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "molstar",
       "name": "molstar",
-      "version": "3.15.0",
+      "version": "3.16.0",
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
         "@types/argparse": "^2.0.10",
         "@types/argparse": "^2.0.10",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "molstar",
   "name": "molstar",
-  "version": "3.15.0",
+  "version": "3.16.0",
   "description": "A comprehensive macromolecular library.",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
   "repository": {

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

@@ -166,7 +166,7 @@ class Viewer {
             structures.push({ ref: structureProperties?.ref || structure.ref });
             structures.push({ ref: structureProperties?.ref || structure.ref });
         }
         }
 
 
-        // remove current structuresfrom hierarchy as they will be merged
+        // remove current structures from hierarchy as they will be merged
         // TODO only works with using loadStructuresFromUrlsAndMerge once
         // TODO only works with using loadStructuresFromUrlsAndMerge once
         //      need some more API metho to work with the hierarchy
         //      need some more API metho to work with the hierarchy
         this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');
         this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');

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

@@ -1,5 +1,5 @@
 /**
 /**
- * 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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
  */
@@ -202,14 +202,14 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
         const components = {
         const components = {
             ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
             ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
             surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
             surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
-            interactions: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `interactions`)
+            interactions: await presetStaticComponent(plugin, structureCell, 'ligand'),
         };
         };
 
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
         const representations = {
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
             ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
             ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
-            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
+            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial, includeParent: true, parentDisplay: 'between' }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
             label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
             label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
         };
         };
 
 

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

@@ -43,6 +43,7 @@ import { MarkingParams } from './passes/marking';
 import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit, GraphicsRenderVariantsDpoit } from '../mol-gl/webgl/render-item';
 import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit, GraphicsRenderVariantsDpoit } from '../mol-gl/webgl/render-item';
 import { degToRad, radToDeg } from '../mol-math/misc';
 import { degToRad, radToDeg } from '../mol-math/misc';
 import { AssetManager } from '../mol-util/assets';
 import { AssetManager } from '../mol-util/assets';
+import { deepClone } from '../mol-util/object';
 
 
 export const Canvas3DParams = {
 export const Canvas3DParams = {
     camera: PD.Group({
     camera: PD.Group({
@@ -291,7 +292,8 @@ namespace Canvas3D {
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
 
     export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
     export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
-        const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
+        const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
+
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
         const reprCount = new BehaviorSubject(0);
         const reprCount = new BehaviorSubject(0);

+ 48 - 14
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -22,6 +22,8 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { InteractionFlag } from '../interactions/common';
 import { InteractionFlag } from '../interactions/common';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
@@ -31,7 +33,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
     const { contacts, unitsFeatures } = interactions;
     const { contacts, unitsFeatures } = interactions;
 
 
     const { edgeCount, edges } = contacts;
     const { edgeCount, edges } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
 
@@ -70,14 +72,48 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 
 
             if (child) {
             if (child) {
                 const b = edges[edgeIndex];
                 const b = edges[edgeIndex];
-                const childUnitA = child.unitMap.get(b.unitA);
-                if (!childUnitA) return true;
-
-                const unitA = structure.unitMap.get(b.unitA);
-                const { offsets, members } = unitsFeatures.get(b.unitA);
-                for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
-                    const eA = unitA.elements[members[i]];
-                    if (!SortedArray.has(childUnitA.elements, eA)) return true;
+
+                if (parentDisplay === 'stub') {
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) return true;
+
+                    const unitA = structure.unitMap.get(b.unitA);
+                    const { offsets, members } = unitsFeatures.get(b.unitA);
+                    for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                        const eA = unitA.elements[members[i]];
+                        if (!SortedArray.has(childUnitA.elements, eA)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) {
+                        flagA = true;
+                    } else {
+                        const unitA = structure.unitMap.get(b.unitA);
+                        const { offsets, members } = unitsFeatures.get(b.unitA);
+                        for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                            const eA = unitA.elements[members[i]];
+                            if (!SortedArray.has(childUnitA.elements, eA)) flagA = true;
+                        }
+                    }
+
+                    const childUnitB = child.unitMap.get(b.unitB);
+                    if (!childUnitB) {
+                        flagB = true;
+                    } else {
+                        const unitB = structure.unitMap.get(b.unitB);
+                        const { offsets, members } = unitsFeatures.get(b.unitB);
+                        for (let i = offsets[b.indexB], il = offsets[b.indexB + 1]; i < il; ++i) {
+                            const eB = unitB.elements[members[i]];
+                            if (!SortedArray.has(childUnitB.elements, eB)) flagB = true;
+                        }
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
                 }
             }
             }
 
 
@@ -101,10 +137,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 export const InteractionsInterUnitParams = {
 export const InteractionsInterUnitParams = {
     ...ComplexMeshParams,
     ...ComplexMeshParams,
     ...LinkCylinderParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 };
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 
 
@@ -121,7 +154,8 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
             );
 
 
             const interactionsHash = InteractionsProvider.get(newStructure).version;
             const interactionsHash = InteractionsProvider.get(newStructure).version;

+ 31 - 10
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -22,6 +22,8 @@ import { Interactions } from '../interactions/interactions';
 import { InteractionFlag } from '../interactions/common';
 import { InteractionFlag } from '../interactions/common';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
@@ -38,7 +40,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 
 
     const { x, y, z, members, offsets } = features;
     const { x, y, z, members, offsets } = features;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
 
@@ -60,10 +62,31 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
 
 
             if (childUnit) {
             if (childUnit) {
-                const f = a[edgeIndex];
-                for (let i = offsets[f], jl = offsets[f + 1]; i < jl; ++i) {
-                    const e = unit.elements[members[offsets[i]]];
-                    if (!SortedArray.has(childUnit.elements, e)) return true;
+                if (parentDisplay === 'stub') {
+                    const f = a[edgeIndex];
+                    for (let i = offsets[f], il = offsets[f + 1]; i < il; ++i) {
+                        const e = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, e)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const fA = a[edgeIndex];
+                    for (let i = offsets[fA], il = offsets[fA + 1]; i < il; ++i) {
+                        const eA = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eA)) flagA = true;
+                    }
+
+                    const fB = b[edgeIndex];
+                    for (let i = offsets[fB], il = offsets[fB + 1]; i < il; ++i) {
+                        const eB = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eB)) flagB = true;
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
                 }
             }
             }
 
 
@@ -86,10 +109,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 export const InteractionsIntraUnitParams = {
 export const InteractionsIntraUnitParams = {
     ...UnitsMeshParams,
     ...UnitsMeshParams,
     ...LinkCylinderParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 };
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 
 
@@ -106,7 +126,8 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
             );
 
 
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;

+ 16 - 0
src/mol-model-props/computed/representations/shared.ts

@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+
+export const InteractionsSharedParams = {
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
+    includeParent: PD.Boolean(false),
+    parentDisplay: PD.Select('stub', PD.arrayToOptions(['stub', 'full', 'between'] as const), { description: 'Only has an effect when "includeParent" is enabled. "Stub" shows just the child side of interactions to the parent. "Full" shows both sides of interactions to the parent. "Between" shows only interactions to the parent.' }),
+};
+export type InteractionsSharedParams = typeof InteractionsSharedParams

+ 21 - 5
src/mol-model/structure/structure/util/superposition-sifts-mapping.ts

@@ -8,7 +8,8 @@
 import { Segmentation } from '../../../../mol-data/int';
 import { Segmentation } from '../../../../mol-data/int';
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import { SIFTSMapping } from '../../../../mol-model-props/sequence/sifts-mapping';
 import { SIFTSMapping } from '../../../../mol-model-props/sequence/sifts-mapping';
-import { ElementIndex } from '../../model/indexing';
+import { ElementIndex, ResidueIndex } from '../../model/indexing';
+import { StructureElement } from '../element';
 import { Structure } from '../structure';
 import { Structure } from '../structure';
 import { Unit } from '../unit';
 import { Unit } from '../unit';
 
 
@@ -24,11 +25,16 @@ export interface AlignmentResult {
     failedPairs: [number, number][]
     failedPairs: [number, number][]
 }
 }
 
 
-export function alignAndSuperposeWithSIFTSMapping(structures: Structure[], options?: { traceOnly?: boolean }): AlignmentResult {
+type IncludeResidueTest = (traceElementOrFirstAtom: StructureElement.Location<Unit.Atomic>, residueIndex: ResidueIndex, startIndex: ElementIndex, endIndex: ElementIndex) => boolean
+
+export function alignAndSuperposeWithSIFTSMapping(
+    structures: Structure[],
+    options?: { traceOnly?: boolean, includeResidueTest?: IncludeResidueTest }
+): AlignmentResult {
     const indexMap = new Map<string, IndexEntry>();
     const indexMap = new Map<string, IndexEntry>();
 
 
     for (let i = 0; i < structures.length; i++) {
     for (let i = 0; i < structures.length; i++) {
-        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true);
+        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true, options?.includeResidueTest ?? _includeAllResidues);
     }
     }
 
 
     const index = Array.from(indexMap.values());
     const index = Array.from(indexMap.values());
@@ -137,11 +143,16 @@ interface IndexEntry {
     pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
     pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
 }
 }
 
 
-function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean) {
+function _includeAllResidues() { return true; }
+
+function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean, includeTest: IncludeResidueTest) {
+    const loc = StructureElement.Location.create<Unit.Atomic>(structure);
+
     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 { elements, model } = unit;
         const { elements, model } = unit;
+        loc.unit = unit;
 
 
         const map = SIFTSMapping.Provider.get(model).value;
         const map = SIFTSMapping.Provider.get(model).value;
         if (!map) return;
         if (!map) return;
@@ -161,9 +172,11 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
 
 
                 if (!dbName[rI]) continue;
                 if (!dbName[rI]) continue;
 
 
+                const traceElement = traceElementIndex[rI];
+
                 let start, end;
                 let start, end;
                 if (traceOnly) {
                 if (traceOnly) {
-                    start = traceElementIndex[rI];
+                    start = traceElement;
                     if (start === -1) continue;
                     if (start === -1) continue;
                     end = start + 1 as ElementIndex;
                     end = start + 1 as ElementIndex;
                 } else {
                 } else {
@@ -171,6 +184,9 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
                     end = elements[residueSegment.end - 1] + 1 as ElementIndex;
                     end = elements[residueSegment.end - 1] + 1 as ElementIndex;
                 }
                 }
 
 
+                loc.element = (traceElement >= 0 ? traceElement : start) as ElementIndex;
+                if (!includeTest(loc, rI, start, end)) continue;
+
                 const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
                 const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
 
 
                 if (!index.has(key)) {
                 if (!index.has(key)) {

+ 21 - 9
src/mol-plugin-ui/viewport/help.tsx

@@ -7,14 +7,15 @@
 import * as React from 'react';
 import * as React from 'react';
 import { Binding } from '../../mol-util/binding';
 import { Binding } from '../../mol-util/binding';
 import { PluginUIComponent } from '../base';
 import { PluginUIComponent } from '../base';
-import { StateTransformer, StateSelection } from '../../mol-state';
+import { StateTransformer, StateSelection, State } from '../../mol-state';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
 import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
 import { Button } from '../controls/common';
 import { Button } from '../controls/common';
+import { memoizeLatest } from '../../mol-util/memoize';
 
 
 function getBindingsList(bindings: { [k: string]: Binding }) {
 function getBindingsList(bindings: { [k: string]: Binding }) {
-    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]);
+    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]).filter(b => Binding.isBinding(b[1]));
 }
 }
 
 
 export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
 export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
@@ -77,19 +78,30 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
     }
     }
 
 
-    render() {
-        const interactionBindings: { [k: string]: Binding } = {};
-        this.plugin.spec.behaviors.forEach(b => {
-            const { bindings } = b.defaultParams;
-            if (bindings) Object.assign(interactionBindings, bindings);
+    getInteractionBindings = memoizeLatest((cells: State.Cells) => {
+        let interactionBindings: { [k: string]: Binding } | undefined = void 0;
+
+        cells.forEach(c => {
+            const params = c.params?.values;
+            if (params?.bindings && Object.keys(params.bindings).length > 0) {
+                if (!interactionBindings) interactionBindings = { };
+                Object.assign(interactionBindings, params.bindings);
+            }
         });
         });
+
+        return interactionBindings;
+    });
+
+    render() {
+        const interactionBindings = this.getInteractionBindings(this.plugin.state.behaviors.cells);
+
         return <>
         return <>
             {(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
             {(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
                 <BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
                 <BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
             </HelpGroup>}
             </HelpGroup>}
-            <HelpGroup key='interactions' header='Mouse Controls'>
+            {!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
                 <BindingsHelp bindings={interactionBindings} />
                 <BindingsHelp bindings={interactionBindings} />
-            </HelpGroup>
+            </HelpGroup>}
         </>;
         </>;
     }
     }
 }
 }

+ 4 - 0
src/mol-util/binding.ts

@@ -24,6 +24,10 @@ namespace Binding {
         return { triggers, action, description };
         return { triggers, action, description };
     }
     }
 
 
+    export function isBinding(x: any): x is Binding {
+        return !!x && Array.isArray(x.triggers) && typeof x.action === 'string';
+    }
+
     export const Empty: Binding = { triggers: [], action: '', description: '' };
     export const Empty: Binding = { triggers: [], action: '', description: '' };
     export function isEmpty(binding: Binding) {
     export function isEmpty(binding: Binding) {
         return binding.triggers.length === 0 ||
         return binding.triggers.length === 0 ||