mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 13:30:24 +08:00
nci: water bridge support
This commit is contained in:
@@ -25,6 +25,7 @@ export type InteractionElementSchema =
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'water-bridge' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
@@ -39,6 +40,7 @@ export const InteractionKinds: InteractionKind[] = [
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'water-bridge',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
@@ -52,6 +54,7 @@ export type InteractionInfo =
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'water-bridge' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
@@ -80,4 +83,5 @@ export const InteractionTypeToKind = {
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.WaterBridge]: 'water-bridge' as InteractionKind,
|
||||
};
|
||||
@@ -47,6 +47,7 @@ export const InteractionVisualParams = {
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'water-bridge': visualParams({ color: Color(0x00CCEE), style: 'dashed' }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
|
||||
@@ -133,6 +133,7 @@ export enum InteractionType {
|
||||
Hydrophobic = 6,
|
||||
MetalCoordination = 7,
|
||||
WeakHydrogenBond = 8,
|
||||
WaterBridge = 9,
|
||||
}
|
||||
|
||||
export function interactionTypeLabel(type: InteractionType): string {
|
||||
@@ -153,6 +154,8 @@ export function interactionTypeLabel(type: InteractionType): string {
|
||||
return 'Pi Stacking';
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return 'Weak Hydrogen Bond';
|
||||
case InteractionType.WaterBridge:
|
||||
return 'Water Bridge';
|
||||
case InteractionType.Unknown:
|
||||
return 'Unknown Interaction';
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import { FeatureType, FeatureGroup, InteractionType } from './common';
|
||||
import { ContactProvider } from './contacts';
|
||||
import { MoleculeType, ProteinBackboneAtoms } from '../../../mol-model/structure/model/types';
|
||||
|
||||
const GeometryParams = {
|
||||
export const GeometryParams = {
|
||||
distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone-to-backbone hydrogen bonds' }),
|
||||
accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
@@ -29,7 +29,7 @@ const GeometryParams = {
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
};
|
||||
type GeometryParams = typeof GeometryParams
|
||||
export type GeometryParams = typeof GeometryParams
|
||||
type GeometryProps = PD.Values<GeometryParams>
|
||||
|
||||
const HydrogenBondsParams = {
|
||||
@@ -208,7 +208,7 @@ function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
|
||||
);
|
||||
}
|
||||
|
||||
function getGeometryOptions(props: GeometryProps) {
|
||||
export function getGeometryOptions(props: GeometryProps) {
|
||||
return {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
@@ -218,7 +218,7 @@ function getGeometryOptions(props: GeometryProps) {
|
||||
maxDonOutOfPlaneAngle: degToRad(props.donOutOfPlaneAngleMax),
|
||||
};
|
||||
}
|
||||
type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
export type GeometryOptions = ReturnType<typeof getGeometryOptions>
|
||||
|
||||
function getHydrogenBondsOptions(props: HydrogenBondsProps) {
|
||||
return {
|
||||
@@ -232,7 +232,7 @@ type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
|
||||
|
||||
const deg120InRad = degToRad(120);
|
||||
|
||||
function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
export function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
const donIndex = don.members[don.offsets[don.feature]];
|
||||
const accIndex = acc.members[acc.offsets[acc.feature]];
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IntMap } from '../../../mol-data/int';
|
||||
import { addUnitContacts, ContactTester, addStructureContacts, ContactsParams, ContactsProps } from './contacts';
|
||||
import { HalogenDonorProvider, HalogenAcceptorProvider, HalogenBondsProvider } from './halogen-bonds';
|
||||
import { HydrogenDonorProvider, WeakHydrogenDonorProvider, HydrogenAcceptorProvider, HydrogenBondsProvider, WeakHydrogenBondsProvider } from './hydrogen-bonds';
|
||||
import { findWaterBridgeContacts, WaterBridgeContacts, WaterBridgesParams } from './water-bridges';
|
||||
import { NegativChargeProvider, PositiveChargeProvider, AromaticRingProvider, IonicProvider, PiStackingProvider, CationPiProvider } from './charged';
|
||||
import { HydrophobicAtomProvider, HydrophobicProvider } from './hydrophobic';
|
||||
import { SetUtils } from '../../../mol-util/set';
|
||||
@@ -37,6 +38,8 @@ interface Interactions {
|
||||
unitsContacts: IntMap<InteractionsIntraContacts>
|
||||
/** Interactions between units */
|
||||
contacts: InteractionsInterContacts
|
||||
/** Water-mediated hydrogen bonds (donor → water → acceptor) */
|
||||
waterBridges: WaterBridgeContacts
|
||||
}
|
||||
|
||||
namespace Interactions {
|
||||
@@ -174,8 +177,16 @@ export const ContactProviderParams = getProvidersParams([
|
||||
// 'weak-hydrogen-bonds',
|
||||
]);
|
||||
|
||||
export const WaterBridgesToggleParams = {
|
||||
'water-bridges': PD.MappedStatic('off', {
|
||||
on: PD.Group(WaterBridgesParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true }),
|
||||
};
|
||||
|
||||
export const InteractionsParams = {
|
||||
providers: PD.Group(ContactProviderParams, { isFlat: true }),
|
||||
waterBridges: PD.Group(WaterBridgesToggleParams, { isFlat: true }),
|
||||
contacts: PD.Group(ContactsParams, { label: 'Advanced Options' }),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
@@ -229,7 +240,12 @@ export async function computeInteractions(ctx: CustomProperty.Context, structure
|
||||
|
||||
const contacts = findInterUnitContacts(structure, unitsFeatures, contactTesters, p.contacts, options);
|
||||
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts };
|
||||
const wbToggle = p.waterBridges['water-bridges'];
|
||||
const waterBridges = wbToggle.name === 'on'
|
||||
? findWaterBridgeContacts(structure, unitsFeatures, wbToggle.params)
|
||||
: [];
|
||||
|
||||
const interactions = { unitsFeatures, unitsContacts, contacts, waterBridges };
|
||||
refineInteractions(structure, interactions);
|
||||
return interactions;
|
||||
}
|
||||
|
||||
243
src/mol-model-props/computed/interactions/water-bridges.ts
Normal file
243
src/mol-model-props/computed/interactions/water-bridges.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
|
||||
import { IntMap } from '../../../mol-data/int';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { DataLocation } from '../../../mol-model/location';
|
||||
import { DataLoci } from '../../../mol-model/loci';
|
||||
import { MoleculeType } from '../../../mol-model/structure/model/types';
|
||||
import { StructureLookup3DResultContext } from '../../../mol-model/structure/structure/util/lookup3d';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
|
||||
import { Features } from './features';
|
||||
import { FeatureType } from './common';
|
||||
import { GeometryParams, GeometryOptions, getGeometryOptions, checkGeometry } from './hydrogen-bonds';
|
||||
import { typeSymbol } from '../chemistry/util';
|
||||
import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
|
||||
import { elementLabel } from '../../../mol-theme/label';
|
||||
|
||||
export type { WaterBridgeContact, WaterBridgeContacts };
|
||||
|
||||
interface WaterBridgeContact {
|
||||
/** non-water donor unit id */
|
||||
readonly unitA: number
|
||||
/** donor feature index in unitA */
|
||||
readonly indexA: Features.FeatureIndex
|
||||
/** non-water acceptor unit id */
|
||||
readonly unitB: number
|
||||
/** acceptor feature index in unitB */
|
||||
readonly indexB: Features.FeatureIndex
|
||||
/** bridging water unit id */
|
||||
readonly unitW: number
|
||||
/** water oxygen as HydrogenAcceptor (leg: donor → water) */
|
||||
readonly indexWA: Features.FeatureIndex
|
||||
/** water oxygen as HydrogenDonor (leg: water → acceptor) */
|
||||
readonly indexWD: Features.FeatureIndex
|
||||
}
|
||||
|
||||
type WaterBridgeContacts = ReadonlyArray<WaterBridgeContact>;
|
||||
|
||||
export const WaterBridgesParams = {
|
||||
...GeometryParams,
|
||||
sulfurDistanceMax: PD.Numeric(4.1, { min: 1, max: 5, step: 0.1 }),
|
||||
};
|
||||
export type WaterBridgesParams = typeof WaterBridgesParams;
|
||||
export type WaterBridgesProps = PD.Values<WaterBridgesParams>;
|
||||
|
||||
function isWater(unit: Unit.Atomic, index: StructureElement.UnitIndex): boolean {
|
||||
return unit.model.atomicHierarchy.derived.residue.moleculeType[
|
||||
unit.residueIndex[unit.elements[index]]
|
||||
] === MoleculeType.Water;
|
||||
}
|
||||
|
||||
const _lookupCtx = StructureLookup3DResultContext();
|
||||
|
||||
export function findWaterBridgeContacts(
|
||||
structure: Structure,
|
||||
unitsFeatures: IntMap<Features>,
|
||||
props: WaterBridgesProps
|
||||
): WaterBridgeContacts {
|
||||
const opts: GeometryOptions = getGeometryOptions(props);
|
||||
const maxLeg = Math.max(props.distanceMax, props.sulfurDistanceMax);
|
||||
const distMaxSq = props.distanceMax * props.distanceMax;
|
||||
const sulfurDistMaxSq = props.sulfurDistanceMax * props.sulfurDistanceMax;
|
||||
|
||||
const edges: WaterBridgeContact[] = [];
|
||||
const wPos = Vec3();
|
||||
|
||||
for (const unitW of structure.units) {
|
||||
if (!Unit.isAtomic(unitW)) continue;
|
||||
const featW = unitsFeatures.get(unitW.id);
|
||||
if (!featW || featW.count === 0) continue;
|
||||
|
||||
// Map each water-oxygen local index to its acceptor and donor feature indices.
|
||||
const waterMap = new Map<StructureElement.UnitIndex, {
|
||||
acc: Features.FeatureIndex | undefined,
|
||||
don: Features.FeatureIndex | undefined
|
||||
}>();
|
||||
|
||||
for (let fi = 0 as Features.FeatureIndex; fi < featW.count; fi++) {
|
||||
const mi = featW.members[featW.offsets[fi]] as StructureElement.UnitIndex;
|
||||
if (!isWater(unitW, mi)) continue;
|
||||
const t = featW.types[fi];
|
||||
if (t !== FeatureType.HydrogenAcceptor && t !== FeatureType.HydrogenDonor) continue;
|
||||
let e = waterMap.get(mi);
|
||||
if (!e) waterMap.set(mi, (e = { acc: undefined, don: undefined }));
|
||||
if (t === FeatureType.HydrogenAcceptor) e.acc = fi;
|
||||
else e.don = fi;
|
||||
}
|
||||
|
||||
for (const [waterAtomIdx, { acc: accFW, don: donFW }] of waterMap) {
|
||||
if (accFW === undefined || donFW === undefined) continue;
|
||||
|
||||
// Water oxygen world-space position.
|
||||
unitW.conformation.position(unitW.elements[waterAtomIdx], wPos);
|
||||
|
||||
// Both Info objects share the same unitW / featW; feature index differs.
|
||||
const infoWAcc = Features.Info(structure, unitW, featW);
|
||||
infoWAcc.feature = accFW;
|
||||
const infoWDon = Features.Info(structure, unitW, featW);
|
||||
infoWDon.feature = donFW;
|
||||
|
||||
// Find all non-water atoms within leg distance of the water oxygen.
|
||||
const { count, indices, units: hitUnits, squaredDistances } =
|
||||
structure.lookup3d.find(wPos[0], wPos[1], wPos[2], maxLeg, _lookupCtx);
|
||||
|
||||
type Candidate = { unitId: number; featureIdx: Features.FeatureIndex };
|
||||
const donors: Candidate[] = [];
|
||||
const acceptors: Candidate[] = [];
|
||||
|
||||
for (let r = 0; r < count; r++) {
|
||||
const hitUnit = hitUnits[r];
|
||||
if (!Unit.isAtomic(hitUnit)) continue;
|
||||
if (hitUnit === unitW) continue;
|
||||
|
||||
const hitLocalIdx = indices[r] as StructureElement.UnitIndex;
|
||||
if (isWater(hitUnit as Unit.Atomic, hitLocalIdx)) continue;
|
||||
|
||||
const hitFeat = unitsFeatures.get(hitUnit.id);
|
||||
if (!hitFeat || hitFeat.count === 0) continue;
|
||||
|
||||
const { indices: fIdxs, offsets: fOff } = hitFeat.elementsIndex;
|
||||
for (let k = fOff[hitLocalIdx], kl = fOff[hitLocalIdx + 1]; k < kl; k++) {
|
||||
const fi = fIdxs[k] as Features.FeatureIndex;
|
||||
const fType = hitFeat.types[fi];
|
||||
if (fType !== FeatureType.HydrogenDonor &&
|
||||
fType !== FeatureType.WeakHydrogenDonor &&
|
||||
fType !== FeatureType.HydrogenAcceptor) continue;
|
||||
|
||||
const memberIdx = hitFeat.members[hitFeat.offsets[fi]] as StructureElement.UnitIndex;
|
||||
const isSulfur = typeSymbol(hitUnit as Unit.Atomic, memberIdx) === Elements.S;
|
||||
if (squaredDistances[r] > (isSulfur ? sulfurDistMaxSq : distMaxSq)) continue;
|
||||
|
||||
if (fType === FeatureType.HydrogenDonor || fType === FeatureType.WeakHydrogenDonor) {
|
||||
const infoDon = Features.Info(structure, hitUnit as Unit.Atomic, hitFeat);
|
||||
infoDon.feature = fi;
|
||||
if (checkGeometry(structure, infoDon, infoWAcc, opts)) {
|
||||
donors.push({ unitId: hitUnit.id, featureIdx: fi });
|
||||
}
|
||||
} else {
|
||||
const infoAcc = Features.Info(structure, hitUnit as Unit.Atomic, hitFeat);
|
||||
infoAcc.feature = fi;
|
||||
if (checkGeometry(structure, infoWDon, infoAcc, opts)) {
|
||||
acceptors.push({ unitId: hitUnit.id, featureIdx: fi });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-pair verified donors and acceptors.
|
||||
for (const don of donors) {
|
||||
for (const acc of acceptors) {
|
||||
// Prevent donor == acceptor (same feature).
|
||||
if (don.unitId === acc.unitId && don.featureIdx === acc.featureIdx) continue;
|
||||
edges.push({
|
||||
unitA: don.unitId, indexA: don.featureIdx,
|
||||
unitB: acc.unitId, indexB: acc.featureIdx,
|
||||
unitW: unitW.id, indexWA: accFW, indexWD: donFW,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Location / Loci for use by the renderer and color theme.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { WaterBridges };
|
||||
|
||||
namespace WaterBridges {
|
||||
export interface Data {
|
||||
readonly structure: Structure
|
||||
readonly waterBridges: WaterBridgeContacts
|
||||
readonly unitsFeatures: IntMap<Features>
|
||||
}
|
||||
|
||||
export interface Element { bridgeIndex: number }
|
||||
|
||||
export interface Location extends DataLocation<Data, Element> {}
|
||||
|
||||
export function Location(data: Data, bridgeIndex = 0): Location {
|
||||
return DataLocation('water-bridges', data, { bridgeIndex });
|
||||
}
|
||||
|
||||
export function isLocation(x: any): x is Location {
|
||||
return !!x && x.kind === 'data-location' && x.tag === 'water-bridges';
|
||||
}
|
||||
|
||||
export interface Loci extends DataLoci<Data, Element> {}
|
||||
|
||||
export function Loci(data: Data, elements: ReadonlyArray<Element>): Loci {
|
||||
return DataLoci('water-bridges', data, elements,
|
||||
bs => getBoundingSphere(data, elements, bs),
|
||||
() => getLabel(data, elements));
|
||||
}
|
||||
|
||||
function getLabel(data: Data, elements: ReadonlyArray<Element>): string {
|
||||
const e = elements[0];
|
||||
if (e === undefined) return '';
|
||||
const { structure, waterBridges, unitsFeatures } = data;
|
||||
const wb = waterBridges[e.bridgeIndex];
|
||||
|
||||
const uA = structure.unitMap.get(wb.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
const uW = structure.unitMap.get(wb.unitW) as Unit.Atomic;
|
||||
const fW = unitsFeatures.get(wb.unitW);
|
||||
const uB = structure.unitMap.get(wb.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
|
||||
const locA = StructureElement.Location.create(structure, uA, uA.elements[fA.members[fA.offsets[wb.indexA]]]);
|
||||
const locW = StructureElement.Location.create(structure, uW, uW.elements[fW.members[fW.offsets[wb.indexWA]]]);
|
||||
const locB = StructureElement.Location.create(structure, uB, uB.elements[fB.members[fB.offsets[wb.indexB]]]);
|
||||
|
||||
return [
|
||||
'Water Bridge',
|
||||
`${elementLabel(locA, { granularity: 'element' })} → ${elementLabel(locW, { granularity: 'residue' })} → ${elementLabel(locB, { granularity: 'element' })}`,
|
||||
].join('</br>');
|
||||
}
|
||||
|
||||
export function isLoci(x: any): x is Loci {
|
||||
return !!x && x.kind === 'data-loci' && x.tag === 'water-bridges';
|
||||
}
|
||||
|
||||
function getBoundingSphere(data: Data, elements: ReadonlyArray<Element>, boundingSphere: Sphere3D) {
|
||||
return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
|
||||
const wb = data.waterBridges[elements[i].bridgeIndex];
|
||||
const uA = data.structure.unitMap.get(wb.unitA) as Unit.Atomic;
|
||||
const fA = data.unitsFeatures.get(wb.unitA);
|
||||
uA.conformation.position(uA.elements[fA.members[fA.offsets[wb.indexA]]], pA);
|
||||
const uB = data.structure.unitMap.get(wb.unitB) as Unit.Atomic;
|
||||
const fB = data.unitsFeatures.get(wb.unitB);
|
||||
uB.conformation.position(uB.elements[fB.members[fB.offsets[wb.indexB]]], pB);
|
||||
}, boundingSphere);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { VisualContext } from '../../../mol-repr/visual';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { createLinkCylinderMesh, LinkCylinderParams, LinkStyle } from '../../../mol-repr/structure/visual/util/link';
|
||||
import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../../../mol-repr/structure/complex-visual';
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { PickingId } from '../../../mol-geo/geometry/picking';
|
||||
import { EmptyLoci, Loci } from '../../../mol-model/loci';
|
||||
import { Interval, OrderedSet } from '../../../mol-data/int';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { WaterBridges } from '../interactions/water-bridges';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { InteractionsSharedParams } from './shared';
|
||||
import { Features } from '../interactions/features';
|
||||
|
||||
function createWaterBridgeCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<WaterBridgeInterUnitParams>, mesh?: Mesh) {
|
||||
if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value!;
|
||||
const { waterBridges, unitsFeatures } = interactions;
|
||||
|
||||
const n = waterBridges.length;
|
||||
if (!n) return Mesh.createEmpty(mesh);
|
||||
|
||||
const l = StructureElement.Location.create(structure);
|
||||
const { sizeFactor } = props;
|
||||
|
||||
const builderProps = {
|
||||
// Four half-cylinders per bridge (createLinkCylinderMesh draws only the A-side half per call):
|
||||
// [0, n): donor→water, forward (donor side)
|
||||
// [n, 2n): donor→water, backward (water side)
|
||||
// [2n, 3n): water→acceptor, forward (water side)
|
||||
// [3n, 4n): water→acceptor, backward (acceptor side)
|
||||
linkCount: 4 * n,
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const wb = waterBridges[edgeIndex % n];
|
||||
const uW = structure.unitMap.get(wb.unitW) as Unit.Atomic;
|
||||
const fW = unitsFeatures.get(wb.unitW);
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
|
||||
if (leg === 0) {
|
||||
// donor→water, A-side: draw donor→mid
|
||||
const uA = structure.unitMap.get(wb.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
atomPosition(uA, fA, wb.indexA, posA);
|
||||
atomPosition(uW, fW, wb.indexWA, posB);
|
||||
} else if (leg === 1) {
|
||||
// donor→water, B-side: draw water→mid (posA/posB swapped)
|
||||
const uA = structure.unitMap.get(wb.unitA) as Unit.Atomic;
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
atomPosition(uW, fW, wb.indexWA, posA);
|
||||
atomPosition(uA, fA, wb.indexA, posB);
|
||||
} else if (leg === 2) {
|
||||
// water→acceptor, A-side: draw water→mid
|
||||
const uB = structure.unitMap.get(wb.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
atomPosition(uW, fW, wb.indexWD, posA);
|
||||
atomPosition(uB, fB, wb.indexB, posB);
|
||||
} else {
|
||||
// water→acceptor, B-side: draw acceptor→mid (posA/posB swapped)
|
||||
const uB = structure.unitMap.get(wb.unitB) as Unit.Atomic;
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
atomPosition(uB, fB, wb.indexB, posA);
|
||||
atomPosition(uW, fW, wb.indexWD, posB);
|
||||
}
|
||||
},
|
||||
style: (_edgeIndex: number) => LinkStyle.Dashed,
|
||||
radius: (edgeIndex: number) => {
|
||||
const wb = waterBridges[edgeIndex % n];
|
||||
const leg = Math.floor(edgeIndex / n);
|
||||
const isDonorWaterLeg = leg <= 1;
|
||||
|
||||
const fA = unitsFeatures.get(isDonorWaterLeg ? wb.unitA : wb.unitW);
|
||||
const unitIdA = isDonorWaterLeg ? wb.unitA : wb.unitW;
|
||||
const indexA = isDonorWaterLeg ? wb.indexA : wb.indexWD;
|
||||
l.unit = structure.unitMap.get(unitIdA);
|
||||
l.element = l.unit.elements[fA.members[fA.offsets[indexA]]];
|
||||
const sizeA = theme.size.size(l);
|
||||
|
||||
const fB = unitsFeatures.get(isDonorWaterLeg ? wb.unitW : wb.unitB);
|
||||
const unitIdB = isDonorWaterLeg ? wb.unitW : wb.unitB;
|
||||
const indexB = isDonorWaterLeg ? wb.indexWA : wb.indexB;
|
||||
l.unit = structure.unitMap.get(unitIdB);
|
||||
l.element = l.unit.elements[fB.members[fB.offsets[indexB]]];
|
||||
const sizeB = theme.size.size(l);
|
||||
|
||||
return Math.min(sizeA, sizeB) * sizeFactor;
|
||||
},
|
||||
ignore: (_edgeIndex: number) => false,
|
||||
};
|
||||
|
||||
const { mesh: m, boundingSphere } = createLinkCylinderMesh(ctx, builderProps, props, mesh);
|
||||
|
||||
if (boundingSphere) {
|
||||
m.setBoundingSphere(boundingSphere);
|
||||
} else if (m.triangleCount > 0) {
|
||||
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
|
||||
m.setBoundingSphere(sphere);
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
function atomPosition(unit: Unit.Atomic, features: Features, featureIndex: Features.FeatureIndex, out: Vec3) {
|
||||
const atomLocalIdx = features.members[features.offsets[featureIndex]];
|
||||
unit.conformation.position(unit.elements[atomLocalIdx], out);
|
||||
}
|
||||
|
||||
export const WaterBridgeInterUnitParams = {
|
||||
...ComplexMeshParams,
|
||||
...LinkCylinderParams,
|
||||
...InteractionsSharedParams,
|
||||
};
|
||||
export type WaterBridgeInterUnitParams = typeof WaterBridgeInterUnitParams
|
||||
|
||||
export function WaterBridgeInterUnitVisual(materialId: number): ComplexVisual<WaterBridgeInterUnitParams> {
|
||||
return ComplexMeshVisual<WaterBridgeInterUnitParams>({
|
||||
defaultProps: PD.getDefaultValues(WaterBridgeInterUnitParams),
|
||||
createGeometry: createWaterBridgeCylinderMesh,
|
||||
createLocationIterator: createWaterBridgeIterator,
|
||||
getLoci: getWaterBridgeLoci,
|
||||
eachLocation: eachWaterBridgeInteraction,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<WaterBridgeInterUnitParams>, currentProps: PD.Values<WaterBridgeInterUnitParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
|
||||
state.createGeometry = (
|
||||
newProps.sizeFactor !== currentProps.sizeFactor ||
|
||||
newProps.dashCount !== currentProps.dashCount ||
|
||||
newProps.dashScale !== currentProps.dashScale ||
|
||||
newProps.dashCap !== currentProps.dashCap ||
|
||||
newProps.radialSegments !== currentProps.radialSegments
|
||||
);
|
||||
|
||||
const interactionsHash = InteractionsProvider.get(newStructure).version;
|
||||
if ((state.info.interactionsHash as number) !== interactionsHash) {
|
||||
state.createGeometry = true;
|
||||
state.updateTransform = true;
|
||||
state.updateColor = true;
|
||||
state.info.interactionsHash = interactionsHash;
|
||||
}
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function getWaterBridgeLoci(pickingId: PickingId, structure: Structure, id: number) {
|
||||
const { objectId, groupId } = pickingId;
|
||||
if (id !== objectId) return EmptyLoci;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value!;
|
||||
const { waterBridges, unitsFeatures } = interactions;
|
||||
const bridgeIndex = groupId % waterBridges.length;
|
||||
|
||||
return WaterBridges.Loci({ structure, waterBridges, unitsFeatures }, [{ bridgeIndex }]);
|
||||
}
|
||||
|
||||
const __unitMap = new Map<number, OrderedSet<StructureElement.UnitIndex>>();
|
||||
|
||||
function eachWaterBridgeInteraction(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, _isMarking: boolean) {
|
||||
let changed = false;
|
||||
|
||||
if (WaterBridges.isLoci(loci)) {
|
||||
if (!Structure.areEquivalent(loci.data.structure, structure)) return false;
|
||||
const interactions = InteractionsProvider.get(structure).value!;
|
||||
const n = interactions.waterBridges.length;
|
||||
|
||||
for (const e of loci.elements) {
|
||||
// Apply all four half-cylinders for this bridge.
|
||||
if (apply(Interval.ofSingleton(e.bridgeIndex))) changed = true;
|
||||
if (apply(Interval.ofSingleton(e.bridgeIndex + n))) changed = true;
|
||||
if (apply(Interval.ofSingleton(e.bridgeIndex + 2 * n))) changed = true;
|
||||
if (apply(Interval.ofSingleton(e.bridgeIndex + 3 * n))) changed = true;
|
||||
}
|
||||
} else if (StructureElement.Loci.is(loci)) {
|
||||
if (!Structure.areEquivalent(loci.structure, structure)) return false;
|
||||
|
||||
const interactions = InteractionsProvider.get(structure).value;
|
||||
if (!interactions) return false;
|
||||
|
||||
const { waterBridges, unitsFeatures } = interactions;
|
||||
const n = waterBridges.length;
|
||||
|
||||
for (const e of loci.elements) __unitMap.set(e.unit.id, e.indices);
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const wb = waterBridges[i];
|
||||
const indicesA = __unitMap.get(wb.unitA);
|
||||
const indicesB = __unitMap.get(wb.unitB);
|
||||
if (!indicesA && !indicesB) continue;
|
||||
|
||||
let hitA = false;
|
||||
if (indicesA) {
|
||||
const fA = unitsFeatures.get(wb.unitA);
|
||||
const mi = fA.members[fA.offsets[wb.indexA]] as StructureElement.UnitIndex;
|
||||
hitA = OrderedSet.has(indicesA, mi);
|
||||
}
|
||||
|
||||
let hitB = false;
|
||||
if (indicesB) {
|
||||
const fB = unitsFeatures.get(wb.unitB);
|
||||
const mi = fB.members[fB.offsets[wb.indexB]] as StructureElement.UnitIndex;
|
||||
hitB = OrderedSet.has(indicesB, mi);
|
||||
}
|
||||
|
||||
if (hitA) {
|
||||
if (apply(Interval.ofSingleton(i))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + n))) changed = true;
|
||||
}
|
||||
if (hitB) {
|
||||
if (apply(Interval.ofSingleton(i + 2 * n))) changed = true;
|
||||
if (apply(Interval.ofSingleton(i + 3 * n))) changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
__unitMap.clear();
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
function createWaterBridgeIterator(structure: Structure): LocationIterator {
|
||||
const interactions = InteractionsProvider.get(structure).value!;
|
||||
const { waterBridges, unitsFeatures } = interactions;
|
||||
const n = waterBridges.length;
|
||||
const groupCount = 4 * n;
|
||||
const instanceCount = 1;
|
||||
const data: WaterBridges.Data = { structure, waterBridges, unitsFeatures };
|
||||
const location = WaterBridges.Location(data);
|
||||
const { element } = location;
|
||||
const getLocation = (groupIndex: number) => {
|
||||
element.bridgeIndex = groupIndex % n;
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation, true);
|
||||
}
|
||||
@@ -12,20 +12,23 @@ import { UnitsRepresentation, StructureRepresentation, StructureRepresentationSt
|
||||
import { InteractionsIntraUnitParams, InteractionsIntraUnitVisual } from './interactions-intra-unit-cylinder';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { InteractionsInterUnitParams, InteractionsInterUnitVisual } from './interactions-inter-unit-cylinder';
|
||||
import { WaterBridgeInterUnitParams, WaterBridgeInterUnitVisual } from './interactions-water-bridge-cylinder';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { getUnitKindsParam } from '../../../mol-repr/structure/params';
|
||||
|
||||
const InteractionsVisuals = {
|
||||
'intra-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsIntraUnitParams>) => UnitsRepresentation('Intra-unit interactions cylinder', ctx, getParams, InteractionsIntraUnitVisual),
|
||||
'inter-unit': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InteractionsInterUnitParams>) => ComplexRepresentation('Inter-unit interactions cylinder', ctx, getParams, InteractionsInterUnitVisual),
|
||||
'water-bridge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, WaterBridgeInterUnitParams>) => ComplexRepresentation('Water bridge cylinder', ctx, getParams, WaterBridgeInterUnitVisual),
|
||||
};
|
||||
|
||||
export const InteractionsParams = {
|
||||
...InteractionsIntraUnitParams,
|
||||
...InteractionsInterUnitParams,
|
||||
...WaterBridgeInterUnitParams,
|
||||
unitKinds: getUnitKindsParam(['atomic']),
|
||||
sizeFactor: PD.Numeric(0.2, { min: 0.01, max: 1, step: 0.01 }),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit'], PD.objectToOptions(InteractionsVisuals)),
|
||||
visuals: PD.MultiSelect(['intra-unit', 'inter-unit', 'water-bridge'], PD.objectToOptions(InteractionsVisuals)),
|
||||
};
|
||||
export type InteractionsParams = typeof InteractionsParams
|
||||
export function getInteractionParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.m.bittrich@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
@@ -13,6 +14,7 @@ import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { InteractionType } from '../interactions/common';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { WaterBridges } from '../interactions/water-bridges';
|
||||
import { CustomProperty } from '../../common/custom-property';
|
||||
import { hash2 } from '../../../mol-data/util';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
@@ -29,6 +31,7 @@ const InteractionTypeColors = ColorMap({
|
||||
CationPi: 0xFF8000,
|
||||
PiStacking: 0x8CB366,
|
||||
WeakHydrogenBond: 0xC5DDEC,
|
||||
WaterBridge: 0x00CCEE,
|
||||
});
|
||||
|
||||
const InteractionTypeColorTable: [string, Color][] = [
|
||||
@@ -40,6 +43,7 @@ const InteractionTypeColorTable: [string, Color][] = [
|
||||
['Cation Pi', InteractionTypeColors.CationPi],
|
||||
['Pi Stacking', InteractionTypeColors.PiStacking],
|
||||
['Weak HydrogenBond', InteractionTypeColors.WeakHydrogenBond],
|
||||
['Water Bridge', InteractionTypeColors.WaterBridge],
|
||||
];
|
||||
|
||||
function typeColor(type: InteractionType): Color {
|
||||
@@ -60,6 +64,8 @@ function typeColor(type: InteractionType): Color {
|
||||
return InteractionTypeColors.PiStacking;
|
||||
case InteractionType.WeakHydrogenBond:
|
||||
return InteractionTypeColors.WeakHydrogenBond;
|
||||
case InteractionType.WaterBridge:
|
||||
return InteractionTypeColors.WaterBridge;
|
||||
case InteractionType.Unknown:
|
||||
return DefaultColor;
|
||||
}
|
||||
@@ -91,6 +97,9 @@ export function InteractionTypeColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
return typeColor(contacts.edges[idx].props.type);
|
||||
}
|
||||
}
|
||||
if (WaterBridges.isLocation(location)) {
|
||||
return InteractionTypeColors.WaterBridge;
|
||||
}
|
||||
return DefaultColor;
|
||||
};
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user