Ver Fonte

dynamic pair bonds on coordinate changes

- add dynamicBonds structure parameter
- add maxRadius, ignoreWater bond compute parameters
- ensure inter unit bond visuals are recreated
Alexander Rose há 3 anos atrás
pai
commit
f9f8350d28

+ 19 - 6
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
         });
     }
 
@@ -398,6 +405,7 @@ class Structure {
         // always assign to ensure object shape
         this._child = asParent?.child;
         this._target = asParent?.target;
+        this._proxy = undefined;
     }
 }
 
@@ -604,6 +612,7 @@ namespace Structure {
     export interface Props {
         parent?: Structure
         interUnitBonds?: InterUnitBonds
+        dynamicBonds?: boolean,
         coordinateSystem?: SymmetryOperator
         label?: string
         /** Master model for structures of a protein model and multiple ligand models */
@@ -683,6 +692,7 @@ namespace Structure {
             polymerResidueCount: -1,
             polymerGapCount: -1,
             polymerUnitCount: -1,
+            dynamicBonds: false,
             coordinateSystem: SymmetryOperator.Default,
             label: ''
         };
@@ -691,6 +701,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 +751,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, dynamicBonds = false): 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, dynamicBonds });
 
         for (let c = 0 as ChainIndex; c < chains.count; c++) {
             const operator = atomicChainOperatorMappinng.get(c) || SymmetryOperator.Default;

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

@@ -205,7 +205,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) {

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

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

@@ -10,10 +10,12 @@ import { ElementSymbol } from '../../../model/types';
 export interface BondComputationProps {
     forceCompute: boolean
     noCompute: boolean
+    maxRadius: number
 }
 export const DefaultBondComputationProps: BondComputationProps = {
     forceCompute: false,
-    noCompute: false
+    noCompute: false,
+    maxRadius: 4,
 };
 
 // H,D,T are all mapped to H

+ 17 - 23
src/mol-model/structure/structure/unit/bonds/inter-compute.ts

@@ -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);
 
@@ -84,8 +84,8 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 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 < maxRadius) {
                     builder.add(_aI, _bI, { order: order[i], flag: flag[i] });
                 }
             }
@@ -102,7 +102,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 +116,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,8 +177,14 @@ 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>();
 
@@ -189,23 +195,10 @@ function findBonds(structure: Structure, props: InterBondComputationProps) {
         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 +207,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 +217,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);
         }),
     });
 }

+ 8 - 5
src/mol-model/structure/structure/unit/bonds/intra-compute.ts

@@ -44,7 +44,9 @@ function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementInd
 
 const __structConnAdded = new Set<StructureElement.UnitIndex>();
 
-function findIndexPairBonds(unit: Unit.Atomic) {
+function findIndexPairBonds(unit: Unit.Atomic, props: BondComputationProps) {
+    const { maxRadius } = props;
+
     const indexPairs = IndexPairBonds.Provider.get(unit.model)!;
     const { elements: atoms } = unit;
     const { type_symbol } = unit.model.atomicHierarchy.atoms;
@@ -74,7 +76,8 @@ function findIndexPairBonds(unit: Unit.Atomic) {
             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 dist = getDistance(unit, aI, bI);
+            if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxRadius) {
                 atomA[atomA.length] = _aI;
                 atomB[atomB.length] = _bI;
                 order[order.length] = edgeProps.order[i];
@@ -87,7 +90,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
 }
 
 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 +171,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);
@@ -246,7 +249,7 @@ function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputatio
     }
 
     if (!p.forceCompute && IndexPairBonds.Provider.get(unit.model)!) {
-        return findIndexPairBonds(unit);
+        return findIndexPairBonds(unit, p);
     } else {
         return findBonds(unit, p);
     }

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

@@ -132,7 +132,7 @@ async function applyCrystalSymmetry(props: { ijkMin: Vec3, ijkMax: Vec3, theme?:
 
     const structure = await builder.createStructure(modelProperties || model, {
         name: 'symmetry',
-        params: props
+        params: { ...props, dynamicBonds: false }
     });
     const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties);
 

+ 5 - 4
src/mol-plugin-state/helpers/root-structure.ts

@@ -52,7 +52,8 @@ export namespace RootStructureDefinition {
             }, { 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' } }),
+                dynamicBonds: PD.Boolean(false),
             }, { isFlat: true }),
             'symmetry-assembly': PD.Group({
                 generators: PD.ObjectList({
@@ -130,8 +131,8 @@ export namespace RootStructureDefinition {
         return new SO.Molecule.Structure(s, props);
     }
 
-    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, dynamicBonds: boolean) {
+        const base = Structure.ofModel(model, dynamicBonds);
         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);
@@ -169,7 +170,7 @@ export namespace RootStructureDefinition {
             return buildAssembly(plugin, ctx, model, params.params.id);
         }
         if (params.name === 'symmetry') {
-            return buildSymmetry(ctx, model, params.params.ijkMin, params.params.ijkMax);
+            return buildSymmetry(ctx, model, params.params.ijkMin, params.params.ijkMax, params.params.dynamicBonds);
         }
         if (params.name === 'symmetry-mates') {
             return buildSymmetryMates(ctx, model, params.params.radius);

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