Browse Source

Merge pull request #238 from molstar/dynbonds

Bond improvements (mostly IndexPairBonds)
Alexander Rose 3 years ago
parent
commit
738b7f4ca5

+ 12 - 8
CHANGELOG.md

@@ -7,18 +7,22 @@ Note that since we don't clearly distinguish between a public and private interf
 ## [Unreleased]
 
 - Add surronding atoms (5 Angstrom) structure selection query
+- [Breaking] Add maxDistance prop to ``IndexPairBonds``
+- Fix coordinateSystem not handled in ``Structure.asParent``
+- Add dynamicBonds to ``Structure`` props (force re-calc on model change)
+    - Expose as optional param in root structure transform helper
 
 ## [v2.2.0] - 2021-07-31
 
-- Add `tubularHelices` parameter to Cartoon representation
-- Add `SdfFormat` and update SDF parser to be able to parse data headers according to spec (hopefully :)) #230
+- Add ``tubularHelices`` parameter to Cartoon representation
+- Add ``SdfFormat`` and update SDF parser to be able to parse data headers according to spec (hopefully :)) #230
 - Fix mononucleotides detected as polymer components (#229)
 - Set default outline scale back to 1
 - Improved DCD reader cell angle handling (interpret near 0 angles as 90 deg)
 - Handle more residue/atom names commonly used in force-fields
 - Add USDZ support to ``geo-export`` extension.
-- Fix `includeParent` support for multi-instance bond visuals.
-- Add `operator` Loci granularity, selecting everything with the same operator name.
+- Fix ``includeParent`` support for multi-instance bond visuals.
+- Add ``operator`` Loci granularity, selecting everything with the same operator name.
 - Prefer ``_label_seq_id`` fields in secondary structure assignment.
 - Support new EMDB API (https://www.ebi.ac.uk/emdb/api/entry/map/[EMBD-ID]) for EM volume contour levels.
 - ``Canvas3D`` tweaks:
@@ -58,8 +62,8 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add ability to select residues from a list of identifiers to the Selection UI.
 - Fix SSAO bugs when used with ``Canvas3D`` viewport.
 - Support for  full pausing (no draw) rendering: ``Canvas3D.pause(true)``.
-- Add `MeshBuilder.addMesh`.
-- Add `Torus` primitive.
+- Add ``MeshBuilder.addMesh``.
+- Add ``Torus`` primitive.
 - Lazy volume loading support.
 - [Breaking] ``Viewer.loadVolumeFromUrl`` signature change.
     - ``loadVolumeFromUrl(url, format, isBinary, isovalues, entryId)`` => ``loadVolumeFromUrl({ url, format, isBinary }, isovalues, { entryId, isLazy })``
@@ -77,12 +81,12 @@ Note that since we don't clearly distinguish between a public and private interf
 - Support for ``ColorTheme.palette`` designed for providing gradient-like coloring.
 
 ### Changed
-- [Breaking] The `zip` function is now asynchronous and expects a `RuntimeContext`. Also added `Zip()` returning a `Task`.
+- [Breaking] The ``zip`` function is now asynchronous and expects a ``RuntimeContext``. Also added ``Zip()`` returning a ``Task``.
 - [Breaking] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension.
 
 ## [v2.0.2] - 2021-03-29
 ### Added
-- `Canvas3D.getRenderObjects`.
+- ``Canvas3D.getRenderObjects``.
 - [WIP] Animate state interpolating, including model trajectories
 
 ### Changed

+ 15 - 6
src/mol-model-formats/structure/property/bonds/index-pair.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,15 +10,17 @@ import { Column } from '../../../../mol-data/db';
 import { FormatPropertyProvider } from '../../common/property';
 import { BondType } from '../../../../mol-model/structure/model/types';
 import { ElementIndex } from '../../../../mol-model/structure';
+import { DefaultBondMaxRadius } from '../../../../mol-model/structure/structure/unit/bonds/common';
 
-export type IndexPairBondsProps = {
+export type IndexPairsProps = {
     readonly order: ArrayLike<number>
     readonly distance: ArrayLike<number>
     readonly flag: ArrayLike<BondType.Flag>
 }
-export type IndexPairBonds = IntAdjacencyGraph<ElementIndex, IndexPairBondsProps>
+export type IndexPairs = IntAdjacencyGraph<ElementIndex, IndexPairsProps>
+export type IndexPairBonds = { bonds: IndexPairs, maxDistance: number }
 
-function getGraph(indexA: ArrayLike<ElementIndex>, indexB: ArrayLike<ElementIndex>, props: Partial<IndexPairBondsProps>, count: number): IndexPairBonds {
+function getGraph(indexA: ArrayLike<ElementIndex>, indexB: ArrayLike<ElementIndex>, props: Partial<IndexPairsProps>, count: number): IndexPairs {
     const builder = new IntAdjacencyGraph.EdgeBuilder(count, indexA, indexB);
     const order = new Int8Array(builder.slotCount);
     const distance = new Array(builder.slotCount);
@@ -51,13 +53,20 @@ export namespace IndexPairBonds {
         count: number
     }
 
-    export function fromData(data: Data) {
+    export const DefaultProps = { maxDistance: DefaultBondMaxRadius };
+    export type Props = typeof DefaultProps
+
+    export function fromData(data: Data, props: Partial<Props> = {}): IndexPairBonds {
+        const p = { ...DefaultProps, ...props };
         const { pairs, count } = data;
         const indexA = pairs.indexA.toArray() as ArrayLike<ElementIndex>;
         const indexB = pairs.indexB.toArray() as ArrayLike<ElementIndex>;
         const order = pairs.order && pairs.order.toArray();
         const distance = pairs.distance && pairs.distance.toArray();
         const flag = pairs.flag && pairs.flag.toArray();
-        return getGraph(indexA, indexB, { order, distance, flag }, count);
+        return {
+            bonds: getGraph(indexA, indexB, { order, distance, flag }, count),
+            maxDistance: p.maxDistance
+        };
     }
 }

+ 30 - 7
src/mol-model/structure/structure/structure.ts

@@ -42,6 +42,7 @@ type State = {
     boundary?: Boundary,
     lookup3d?: StructureLookup3D,
     interUnitBonds?: InterUnitBonds,
+    dynamicBonds: boolean,
     unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
     unitSymmetryGroupsIndexMap?: IntMap<number>,
     unitsSortedByVolume?: ReadonlyArray<Unit>;
@@ -231,10 +232,14 @@ class Structure {
 
     get interUnitBonds() {
         if (this.state.interUnitBonds) return this.state.interUnitBonds;
-        this.state.interUnitBonds = computeInterUnitBonds(this);
+        this.state.interUnitBonds = computeInterUnitBonds(this, { ignoreWater: !this.dynamicBonds });
         return this.state.interUnitBonds;
     }
 
+    get dynamicBonds() {
+        return this.state.dynamicBonds;
+    }
+
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
         if (this.state.unitSymmetryGroups) return this.state.unitSymmetryGroups;
         this.state.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
@@ -351,18 +356,20 @@ class Structure {
     }
 
     remapModel(m: Model) {
+        const { dynamicBonds, interUnitBonds } = this.state;
         const units: Unit[] = [];
         for (const ug of this.unitSymmetryGroups) {
-            const unit = ug.units[0].remapModel(m);
+            const unit = ug.units[0].remapModel(m, dynamicBonds);
             units.push(unit);
             for (let i = 1, il = ug.units.length; i < il; ++i) {
                 const u = ug.units[i];
-                units.push(u.remapModel(m, unit.props));
+                units.push(u.remapModel(m, dynamicBonds, unit.props));
             }
         }
         return Structure.create(units, {
             label: this.label,
-            interUnitBonds: this.state.interUnitBonds,
+            interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
+            dynamicBonds
         });
     }
 
@@ -376,7 +383,13 @@ class Structure {
      */
     asParent(): Structure {
         if (this._proxy) return this._proxy;
-        this._proxy = this.parent ? new Structure(this.parent.units, this.parent.unitMap, this.parent.unitIndexMap, this.parent.state, { child: this, target: this.parent }) : this;
+        if (this.parent) {
+            const p = this.parent.coordinateSystem.isIdentity ? this.parent : Structure.transform(this.parent, this.parent.coordinateSystem.inverse);
+            const s = this.coordinateSystem.isIdentity ? p : Structure.transform(p, this.coordinateSystem.matrix);
+            this._proxy = new Structure(s.units, s.unitMap, s.unitIndexMap, { ...s.state, dynamicBonds: this.dynamicBonds }, { child: this, target: this.parent });
+        } else {
+            this._proxy = this;
+        }
         return this._proxy;
     }
 
@@ -398,6 +411,7 @@ class Structure {
         // always assign to ensure object shape
         this._child = asParent?.child;
         this._target = asParent?.target;
+        this._proxy = undefined;
     }
 }
 
@@ -604,6 +618,11 @@ namespace Structure {
     export interface Props {
         parent?: Structure
         interUnitBonds?: InterUnitBonds
+        /**
+         * Ensure bonds are recalculated upon model changes.
+         * Also enables calculation of inter-unit bonds in water molecules.
+         */
+        dynamicBonds?: boolean,
         coordinateSystem?: SymmetryOperator
         label?: string
         /** Master model for structures of a protein model and multiple ligand models */
@@ -683,6 +702,7 @@ namespace Structure {
             polymerResidueCount: -1,
             polymerGapCount: -1,
             polymerUnitCount: -1,
+            dynamicBonds: false,
             coordinateSystem: SymmetryOperator.Default,
             label: ''
         };
@@ -691,6 +711,9 @@ namespace Structure {
         if (props.parent) state.parent = props.parent.parent || props.parent;
         if (props.interUnitBonds) state.interUnitBonds = props.interUnitBonds;
 
+        if (props.dynamicBonds) state.dynamicBonds = props.dynamicBonds;
+        else if (props.parent) state.dynamicBonds = props.parent.dynamicBonds;
+
         if (props.coordinateSystem) state.coordinateSystem = props.coordinateSystem;
         else if (props.parent) state.coordinateSystem = props.parent.coordinateSystem;
 
@@ -738,12 +761,12 @@ namespace Structure {
      * Generally, a single unit corresponds to a single chain, with the exception
      * of consecutive "single atom chains" with same entity_id and same auth_asym_id.
      */
-    export function ofModel(model: Model): Structure {
+    export function ofModel(model: Model, props: Props = {}): Structure {
         const chains = model.atomicHierarchy.chainAtomSegments;
         const { index } = model.atomicHierarchy;
         const { auth_asym_id } = model.atomicHierarchy.chains;
         const { atomicChainOperatorMappinng } = model;
-        const builder = new StructureBuilder({ label: model.label });
+        const builder = new StructureBuilder({ label: model.label, ...props });
 
         for (let c = 0 as ChainIndex; c < chains.count; c++) {
             const operator = atomicChainOperatorMappinng.get(c) || SymmetryOperator.Default;

+ 19 - 5
src/mol-model/structure/structure/symmetry.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -27,7 +27,11 @@ namespace StructureSymmetry {
             if (!assembly) throw new Error(`Assembly '${asmName}' is not defined.`);
 
             const coordinateSystem = SymmetryOperator.create(assembly.id, Mat4.identity(), { assembly: { id: assembly.id, operId: 0, operList: [] } });
-            const assembler = Structure.Builder({ coordinateSystem, label: structure.label });
+            const assembler = Structure.Builder({
+                coordinateSystem,
+                label: structure.label,
+                dynamicBonds: structure.dynamicBonds
+            });
 
             const queryCtx = new QueryContext(structure);
 
@@ -57,7 +61,11 @@ namespace StructureSymmetry {
             if (models.length !== 1) throw new Error('Can only build symmetry assemblies from structures based on 1 model.');
 
             const modelCenter = Vec3();
-            const assembler = Structure.Builder({ label: structure.label, representativeModel: models[0] });
+            const assembler = Structure.Builder({
+                label: structure.label,
+                representativeModel: models[0],
+                dynamicBonds: structure.dynamicBonds
+            });
 
             const queryCtx = new QueryContext(structure);
 
@@ -205,7 +213,10 @@ function getOperatorsCached333(symmetry: Symmetry, ref: Vec3) {
 }
 
 function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) {
-    const assembler = Structure.Builder({ label: structure.label });
+    const assembler = Structure.Builder({
+        label: structure.label,
+        dynamicBonds: structure.dynamicBonds
+    });
     const { units } = structure;
     for (const oper of operators) {
         for (const unit of units) {
@@ -263,7 +274,10 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
         return `${unit.invariantId}|${oper.name}`;
     }
 
-    const assembler = Structure.Builder({ label: structure.label });
+    const assembler = Structure.Builder({
+        label: structure.label,
+        dynamicBonds: structure.dynamicBonds
+    });
 
     const { units } = structure;
     const center = Vec3.zero();

+ 4 - 4
src/mol-model/structure/structure/unit.ts

@@ -144,7 +144,7 @@ namespace Unit {
 
         getChild(elements: StructureElement.Set): Unit,
         applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
-        remapModel(model: Model): Unit,
+        remapModel(model: Model, dynamicBonds: boolean): Unit,
 
         readonly boundary: Boundary
         readonly lookup3d: Lookup3D<StructureElement.UnitIndex>
@@ -218,9 +218,9 @@ namespace Unit {
             return new Atomic(id, this.invariantId, this.chainGroupId, this.traits, this.model, this.elements, SymmetryOperator.createMapping(op, this.model.atomicConformation, this.conformation.r), this.props);
         }
 
-        remapModel(model: Model, props?: AtomicProperties) {
+        remapModel(model: Model, dynamicBonds: boolean, props?: AtomicProperties) {
             if (!props) {
-                props = { ...this.props, bonds: tryRemapBonds(this, this.props.bonds, model) };
+                props = { ...this.props, bonds: dynamicBonds ? undefined : tryRemapBonds(this, this.props.bonds, model) };
                 if (!Unit.isSameConformation(this, model)) {
                     props.boundary = undefined;
                     props.lookup3d = undefined;
@@ -378,7 +378,7 @@ namespace Unit {
             return createCoarse(id, this.invariantId, this.chainGroupId, this.traits, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseConformation(), this.conformation.r), this.props);
         }
 
-        remapModel(model: Model, props?: CoarseProperties): Unit.Spheres | Unit.Gaussians {
+        remapModel(model: Model, dynamicBonds: boolean, props?: CoarseProperties): Unit.Spheres | Unit.Gaussians {
             const coarseConformation = this.getCoarseConformation();
             const modelCoarseConformation = getCoarseConformation(this.kind, model);
 

+ 6 - 1
src/mol-model/structure/structure/unit/bonds/common.ts

@@ -7,13 +7,18 @@
 
 import { ElementSymbol } from '../../../model/types';
 
+/** Default for atomic bonds */
+export const DefaultBondMaxRadius = 4;
+
 export interface BondComputationProps {
     forceCompute: boolean
     noCompute: boolean
+    maxRadius: number
 }
 export const DefaultBondComputationProps: BondComputationProps = {
     forceCompute: false,
-    noCompute: false
+    noCompute: false,
+    maxRadius: DefaultBondMaxRadius,
 };
 
 // H,D,T are all mapped to H

+ 24 - 28
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -21,8 +21,6 @@ import { StructConn } from '../../../../../mol-model-formats/structure/property/
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { Model } from '../../../model';
 
-const MAX_RADIUS = 4;
-
 const tmpDistVecA = Vec3();
 const tmpDistVecB = Vec3();
 function getDistance(unitA: Unit.Atomic, indexA: ElementIndex, unitB: Unit.Atomic, indexB: ElementIndex) {
@@ -35,6 +33,8 @@ const _imageTransform = Mat4();
 const _imageA = Vec3();
 
 function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComputationProps, builder: InterUnitGraph.Builder<number, StructureElement.UnitIndex, InterUnitEdgeProps>) {
+    const { maxRadius } = props;
+
     const { elements: atomsA, residueIndex: residueIndexA } = unitA;
     const { x: xA, y: yA, z: zA } = unitA.model.atomicConformation;
     const { elements: atomsB, residueIndex: residueIndexB } = unitB;
@@ -62,7 +62,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
     const isNotIdentity = !Mat4.isIdentity(imageTransform);
 
     const { center: bCenter, radius: bRadius } = unitB.boundary.sphere;
-    const testDistanceSq = (bRadius + MAX_RADIUS) * (bRadius + MAX_RADIUS);
+    const testDistanceSq = (bRadius + maxRadius) * (bRadius + maxRadius);
 
     builder.startUnitPair(unitA.id, unitB.id);
 
@@ -73,19 +73,20 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
         if (Vec3.squaredDistance(_imageA, bCenter) > testDistanceSq) continue;
 
         if (!props.forceCompute && indexPairs) {
-            const { order, distance, flag } = indexPairs.edgeProps;
+            const { maxDistance } = indexPairs;
+            const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
 
             const srcA = sourceIndex.value(aI);
-            for (let i = indexPairs.offset[srcA], il = indexPairs.offset[srcA + 1]; i < il; ++i) {
-                const bI = invertedIndex![indexPairs.b[i]];
+            for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) {
+                const bI = invertedIndex![b[i]];
 
                 const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex;
                 if (_bI < 0) continue;
                 if (type_symbolA.value(aI) === 'H' && type_symbolB.value(bI) === 'H') continue;
 
                 const d = distance[i];
-                // only allow inter-unit index-pair bonds when a distance is given
-                if (d !== -1 && equalEps(getDistance(unitA, aI, unitB, bI), d, 0.5)) {
+                const dist = getDistance(unitA, aI, unitB, bI);
+                if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxDistance) {
                     builder.add(_aI, _bI, { order: order[i], flag: flag[i] });
                 }
             }
@@ -102,7 +103,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 if (_bI < 0) continue;
 
                 // check if the bond is within MAX_RADIUS for this pair of units
-                if (getDistance(unitA, aI, unitB, p.atomIndex) > MAX_RADIUS) continue;
+                if (getDistance(unitA, aI, unitB, p.atomIndex) > maxRadius) continue;
 
                 builder.add(_aI, _bI, { order: se.order, flag: se.flags });
                 added = true;
@@ -116,7 +117,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
         const occA = occupancyA.value(aI);
 
         const { lookup3d } = unitB;
-        const { indices, count, squaredDistances } = lookup3d.find(_imageA[0], _imageA[1], _imageA[2], MAX_RADIUS);
+        const { indices, count, squaredDistances } = lookup3d.find(_imageA[0], _imageA[1], _imageA[2], maxRadius);
         if (count === 0) continue;
 
         const aeI = getElementIdx(type_symbolA.value(aI));
@@ -177,35 +178,29 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 
 export interface InterBondComputationProps extends BondComputationProps {
     validUnitPair: (structure: Structure, unitA: Unit, unitB: Unit) => boolean
+    ignoreWater: boolean
 }
 
+const DefaultInterBondComputationProps = {
+    ...DefaultBondComputationProps,
+    ignoreWater: true
+};
+
 function findBonds(structure: Structure, props: InterBondComputationProps) {
     const builder = new InterUnitGraph.Builder<number, StructureElement.UnitIndex, InterUnitEdgeProps>();
+    const hasIndexPairBonds = structure.models.some(m => IndexPairBonds.Provider.get(m));
 
-    if (props.noCompute || structure.isCoarseGrained) {
+    if (props.noCompute || (structure.isCoarseGrained && !hasIndexPairBonds)) {
         // TODO add function that only adds bonds defined in structConn and avoids using
         //      structure.lookup and unit.lookup (expensive for large structure and not
         //      needed for archival files or files with an MD topology)
         return new InterUnitBonds(builder.getMap());
     }
 
-    const indexPairs = structure.models.length === 1 && IndexPairBonds.Provider.get(structure.model);
-    if (indexPairs) {
-        const { distance } = indexPairs.edgeProps;
-        let hasDistance = false;
-        for (let i = 0, il = distance.length; i < il; ++i) {
-            if (distance[i] !== -1) {
-                hasDistance = true;
-                break;
-            }
-        }
-        if (!hasDistance) return new InterUnitBonds(builder.getMap());
-    }
-
     Structure.eachUnitPair(structure, (unitA: Unit, unitB: Unit) => {
         findPairBonds(unitA as Unit.Atomic, unitB as Unit.Atomic, props, builder);
     }, {
-        maxRadius: MAX_RADIUS,
+        maxRadius: props.maxRadius,
         validUnit: (unit: Unit) => Unit.isAtomic(unit),
         validUnitPair: (unitA: Unit, unitB: Unit) => props.validUnitPair(structure, unitA, unitB)
     });
@@ -214,8 +209,9 @@ function findBonds(structure: Structure, props: InterBondComputationProps) {
 }
 
 function computeInterUnitBonds(structure: Structure, props?: Partial<InterBondComputationProps>): InterUnitBonds {
+    const p = { ...DefaultInterBondComputationProps, ...props };
     return findBonds(structure, {
-        ...DefaultBondComputationProps,
+        ...p,
         validUnitPair: (props && props.validUnitPair) || ((s, a, b) => {
             const mtA = a.model.atomicHierarchy.derived.residue.moleculeType;
             const mtB = b.model.atomicHierarchy.derived.residue.moleculeType;
@@ -223,7 +219,7 @@ function computeInterUnitBonds(structure: Structure, props?: Partial<InterBondCo
                 (!Unit.isAtomic(a) || mtA[a.residueIndex[a.elements[0]]] !== MoleculeType.Water) &&
                 (!Unit.isAtomic(b) || mtB[b.residueIndex[b.elements[0]]] !== MoleculeType.Water)
             );
-            return Structure.validUnitPair(s, a, b) && notWater;
+            return Structure.validUnitPair(s, a, b) && (notWater || !p.ignoreWater);
         }),
     });
 }

+ 15 - 13
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -49,7 +49,8 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const { elements: atoms } = unit;
     const { type_symbol } = unit.model.atomicHierarchy.atoms;
     const atomCount = unit.elements.length;
-    const { edgeProps } = indexPairs;
+    const { maxDistance } = indexPairs;
+    const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
 
     const { atomSourceIndex: sourceIndex } = unit.model.atomicHierarchy;
     const { invertedIndex } = Model.getInvertedAtomSourceIndex(unit.model);
@@ -57,7 +58,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const atomA: StructureElement.UnitIndex[] = [];
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
-    const order: number[] = [];
+    const orders: number[] = [];
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI =  atoms[_aI];
@@ -65,29 +66,30 @@ function findIndexPairBonds(unit: Unit.Atomic) {
 
         const srcA = sourceIndex.value(aI);
 
-        for (let i = indexPairs.offset[srcA], il = indexPairs.offset[srcA + 1]; i < il; ++i) {
-            const bI = invertedIndex[indexPairs.b[i]];
+        for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) {
+            const bI = invertedIndex[b[i]];
             if (aI >= bI) continue;
 
             const _bI = SortedArray.indexOf(unit.elements, bI) as StructureElement.UnitIndex;
             if (_bI < 0) continue;
             if (isHa && type_symbol.value(bI) === 'H') continue;
 
-            const d = edgeProps.distance[i];
-            if (d === -1 || d === void 0 || equalEps(getDistance(unit, aI, bI), d, 0.5)) {
+            const d = distance[i];
+            const dist = getDistance(unit, aI, bI);
+            if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxDistance) {
                 atomA[atomA.length] = _aI;
                 atomB[atomB.length] = _bI;
-                order[order.length] = edgeProps.order[i];
-                flags[flags.length] = edgeProps.flag[i];
+                orders[order.length] = order[i];
+                flags[flags.length] = flag[i];
             }
         }
     }
 
-    return getGraph(atomA, atomB, order, flags, atomCount, false);
+    return getGraph(atomA, atomB, orders, flags, atomCount, false);
 }
 
 function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
-    const MAX_RADIUS = 4;
+    const { maxRadius } = props;
 
     const { x, y, z } = unit.model.atomicConformation;
     const atomCount = unit.elements.length;
@@ -168,7 +170,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
         const atomIdA = label_atom_id.value(aI);
         const componentPairs = componentMap ? componentMap.get(atomIdA) : void 0;
 
-        const { indices, count, squaredDistances } = query3d.find(x[aI], y[aI], z[aI], MAX_RADIUS);
+        const { indices, count, squaredDistances } = query3d.find(x[aI], y[aI], z[aI], maxRadius);
         const isHa = isHydrogen(aeI);
         const thresholdA = getElementThreshold(aeI);
         const altA = label_alt_id.value(aI);
@@ -245,7 +247,7 @@ function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputatio
         return IntraUnitBonds.Empty;
     }
 
-    if (!p.forceCompute && IndexPairBonds.Provider.get(unit.model)!) {
+    if (!p.forceCompute && IndexPairBonds.Provider.get(unit.model)) {
         return findIndexPairBonds(unit);
     } else {
         return findBonds(unit, p);

+ 1 - 1
src/mol-plugin-state/builder/structure/hierarchy-preset.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>

+ 40 - 30
src/mol-plugin-state/helpers/root-structure.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -16,6 +16,11 @@ import { Assembly, Symmetry } from '../../mol-model/structure/model/properties/s
 import { PluginStateObject as SO } from '../objects';
 import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
 
+const CommonStructureParams = {
+    dynamicBonds: PD.Optional(PD.Boolean(false, { description: 'Ensure bonds are recalculated upon model changes. Also enables calculation of inter-unit bonds in water molecules.' })),
+};
+type CommonStructureProps = PD.ValuesFor<typeof CommonStructureParams>
+
 export namespace RootStructureDefinition {
     export function getParams(model?: Model, defaultValue?: 'auto' | 'model' | 'assembly' | 'symmetry' | 'symmetry-mates' | 'symmetry-assembly') {
         const symmetry = model && ModelSymmetry.Provider.get(model);
@@ -40,19 +45,22 @@ export namespace RootStructureDefinition {
         }
 
         const modes = {
-            auto: PD.EmptyGroup(),
-            model: PD.EmptyGroup(),
+            auto: PD.Group(CommonStructureParams),
+            model: PD.Group(CommonStructureParams),
             assembly: PD.Group({
                 id: PD.Optional(model
                     ? PD.Select(assemblyIds.length ? assemblyIds[0][0] : '', assemblyIds, { label: 'Asm Id', description: 'Assembly Id' })
-                    : PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' }))
+                    : PD.Text('', { label: 'Asm Id', description: 'Assembly Id (use empty for the 1st assembly)' })),
+                ...CommonStructureParams
             }, { isFlat: true }),
             'symmetry-mates': PD.Group({
-                radius: PD.Numeric(5, { min: 0, max: 50, step: 1 })
+                radius: PD.Numeric(5, { min: 0, max: 50, step: 1 }),
+                ...CommonStructureParams
             }, { isFlat: true }),
             'symmetry': PD.Group({
                 ijkMin: PD.Vec3(Vec3.create(-1, -1, -1), { step: 1 }, { label: 'Min IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
-                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { step: 1 }, { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } })
+                ijkMax: PD.Vec3(Vec3.create(1, 1, 1), { step: 1 }, { label: 'Max IJK', fieldLabels: { x: 'I', y: 'J', z: 'K' } }),
+                ...CommonStructureParams
             }, { isFlat: true }),
             'symmetry-assembly': PD.Group({
                 generators: PD.ObjectList({
@@ -65,7 +73,8 @@ export namespace RootStructureDefinition {
                     asymIds: PD.MultiSelect([] as string[], asymIdsOptions)
                 }, e => `${e.asymIds.length} asym ids, ${e.operators.length} operators`, {
                     defaultValue: [] as { operators: { index: number, shift: Vec3 }[], asymIds: string[] }[]
-                })
+                }),
+                ...CommonStructureParams
             }, { isFlat: true })
         };
 
@@ -99,7 +108,7 @@ export namespace RootStructureDefinition {
         return true;
     }
 
-    async function buildAssembly(plugin: PluginContext, ctx: RuntimeContext, model: Model, id?: string) {
+    async function buildAssembly(plugin: PluginContext, ctx: RuntimeContext, model: Model, id?: string, props?: CommonStructureProps) {
         let asm: Assembly | undefined = void 0;
 
         const symmetry = ModelSymmetry.Provider.get(model);
@@ -118,7 +127,7 @@ export namespace RootStructureDefinition {
             }
         }
 
-        const base = Structure.ofModel(model);
+        const base = Structure.ofModel(model, props);
         if (!asm) {
             const label = { label: 'Model', description: Structure.elementDescription(base) };
             return new SO.Molecule.Structure(base, label);
@@ -126,56 +135,57 @@ export namespace RootStructureDefinition {
 
         id = asm.id;
         const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
-        const props = { label: `Assembly ${id}`, description: Structure.elementDescription(s) };
-        return new SO.Molecule.Structure(s, props);
+        const objProps = { label: `Assembly ${id}`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, objProps);
     }
 
-    async function buildSymmetry(ctx: RuntimeContext, model: Model, ijkMin: Vec3, ijkMax: Vec3) {
-        const base = Structure.ofModel(model);
+    async function buildSymmetry(ctx: RuntimeContext, model: Model, ijkMin: Vec3, ijkMax: Vec3, props?: CommonStructureProps) {
+        const base = Structure.ofModel(model, props);
         const s = await StructureSymmetry.buildSymmetryRange(base, ijkMin, ijkMax).runInContext(ctx);
-        const props = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: Structure.elementDescription(s) };
-        return new SO.Molecule.Structure(s, props);
+        const objProps = { label: `Symmetry [${ijkMin}] to [${ijkMax}]`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, objProps);
     }
 
-    async function buildSymmetryMates(ctx: RuntimeContext, model: Model, radius: number) {
-        const base = Structure.ofModel(model);
+    async function buildSymmetryMates(ctx: RuntimeContext, model: Model, radius: number, props?: CommonStructureProps) {
+        const base = Structure.ofModel(model, props);
         const s = await StructureSymmetry.builderSymmetryMates(base, radius).runInContext(ctx);
-        const props = { label: `Symmetry Mates`, description: Structure.elementDescription(s) };
-        return new SO.Molecule.Structure(s, props);
+        const objProps = { label: `Symmetry Mates`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, objProps);
     }
 
-    async function buildSymmetryAssembly(ctx: RuntimeContext, model: Model, generators: StructureSymmetry.Generators, symmetry: Symmetry) {
-        const base = Structure.ofModel(model);
+    async function buildSymmetryAssembly(ctx: RuntimeContext, model: Model, generators: StructureSymmetry.Generators, symmetry: Symmetry, props?: CommonStructureProps) {
+        const base = Structure.ofModel(model, props);
         const s = await StructureSymmetry.buildSymmetryAssembly(base, generators, symmetry).runInContext(ctx);
-        const props = { label: `Symmetry Assembly`, description: Structure.elementDescription(s) };
-        return new SO.Molecule.Structure(s, props);
+        const objProps = { label: `Symmetry Assembly`, description: Structure.elementDescription(s) };
+        return new SO.Molecule.Structure(s, objProps);
     }
 
     export async function create(plugin: PluginContext, ctx: RuntimeContext, model: Model, params?: Params): Promise<SO.Molecule.Structure> {
+        const props = params?.params;
         const symmetry = ModelSymmetry.Provider.get(model);
         if (!symmetry || !params || params.name === 'model') {
-            const s = Structure.ofModel(model);
+            const s = Structure.ofModel(model, props);
             return new SO.Molecule.Structure(s, { label: 'Model', description: Structure.elementDescription(s) });
         }
         if (params.name === 'auto') {
             if (symmetry.assemblies.length === 0) {
-                const s = Structure.ofModel(model);
+                const s = Structure.ofModel(model, props);
                 return new SO.Molecule.Structure(s, { label: 'Model', description: Structure.elementDescription(s) });
             } else {
-                return buildAssembly(plugin, ctx, model);
+                return buildAssembly(plugin, ctx, model, undefined, props);
             }
         }
         if (params.name === 'assembly') {
-            return buildAssembly(plugin, ctx, model, params.params.id);
+            return buildAssembly(plugin, ctx, model, params.params.id, props);
         }
         if (params.name === 'symmetry') {
-            return buildSymmetry(ctx, model, params.params.ijkMin, params.params.ijkMax);
+            return buildSymmetry(ctx, model, params.params.ijkMin, params.params.ijkMax, props);
         }
         if (params.name === 'symmetry-mates') {
-            return buildSymmetryMates(ctx, model, params.params.radius);
+            return buildSymmetryMates(ctx, model, params.params.radius, props);
         }
         if (params.name === 'symmetry-assembly') {
-            return buildSymmetryAssembly(ctx, model, params.params.generators, symmetry);
+            return buildSymmetryAssembly(ctx, model, params.params.generators, symmetry, props);
         }
 
         throw new Error(`Unknown represetation type: ${(params as any).name}`);

+ 16 - 2
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -203,7 +203,7 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
         createLocationIterator: BondIterator.fromStructure,
         getLoci: getInterBondLoci,
         eachLocation: eachInterBond,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>) => {
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
             state.createGeometry = (
                 newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
                 newProps.linkScale !== currentProps.linkScale ||
@@ -218,6 +218,13 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
             );
+
+            if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
+                state.createGeometry = true;
+                state.updateTransform = true;
+                state.updateColor = true;
+                state.updateSize = true;
+            }
         },
         mustRecreate: (structure: Structure, props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
             return !props.tryUseImpostor || !webgl;
@@ -232,7 +239,7 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu
         createLocationIterator: BondIterator.fromStructure,
         getLoci: getInterBondLoci,
         eachLocation: eachInterBond,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>) => {
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.sizeAspectRatio !== currentProps.sizeAspectRatio ||
@@ -249,6 +256,13 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) ||
                 newProps.adjustCylinderLength !== currentProps.adjustCylinderLength
             );
+
+            if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
+                state.createGeometry = true;
+                state.updateTransform = true;
+                state.updateColor = true;
+                state.updateSize = true;
+            }
         },
         mustRecreate: (structure: Structure, props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
             return props.tryUseImpostor && !!webgl;

+ 8 - 1
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -120,7 +120,7 @@ export function InterUnitBondLineVisual(materialId: number): ComplexVisual<Inter
         createLocationIterator: BondIterator.fromStructure,
         getLoci: getInterBondLoci,
         eachLocation: eachInterBond,
-        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondLineParams>, currentProps: PD.Values<InterUnitBondLineParams>) => {
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondLineParams>, currentProps: PD.Values<InterUnitBondLineParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linkScale !== currentProps.linkScale ||
@@ -130,6 +130,13 @@ export function InterUnitBondLineVisual(materialId: number): ComplexVisual<Inter
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
+
+            if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) {
+                state.createGeometry = true;
+                state.updateTransform = true;
+                state.updateColor = true;
+                state.updateSize = true;
+            }
         }
     }, materialId);
 }