diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a27ba48d5472cb1bd587ebace00c5a1bf5f25a0..6621454c76252a5be2baf50c911a9551593018e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- Support `ignoreHydrogens` in interactions representation + ## [v3.34.0] - 2023-04-16 - Avoid `renderMarkingDepth` for fully transparent renderables diff --git a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts index 9a0d9164212b5f844665d350b75a970d7621513c..f8d421e8d6fdcc530f5fd8b469a46565546db06a 100644 --- a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts +++ b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -19,11 +19,13 @@ import { Interval, OrderedSet, SortedArray } from '../../../mol-data/int'; import { Interactions } from '../interactions/interactions'; import { InteractionsProvider } from '../interactions'; import { LocationIterator } from '../../../mol-geo/util/location-iterator'; -import { InteractionFlag } from '../interactions/common'; +import { InteractionFlag, InteractionType } from '../interactions/common'; import { Unit } from '../../../mol-model/structure/structure'; import { Sphere3D } from '../../../mol-math/geometry'; import { assertUnreachable } from '../../../mol-util/type-helpers'; import { InteractionsSharedParams } from './shared'; +import { eachBondedAtom } from '../chemistry/util'; +import { isHydrogen } from '../../../mol-repr/structure/visual/util/common'; function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) { if (!structure.hasAtomic) return Mesh.createEmpty(mesh); @@ -33,26 +35,66 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S const { contacts, unitsFeatures } = interactions; const { edgeCount, edges } = contacts; - const { sizeFactor, parentDisplay } = props; + const { sizeFactor, ignoreHydrogens, ignoreHydrogensVariant, parentDisplay } = props; if (!edgeCount) return Mesh.createEmpty(mesh); const { child } = structure; + const p = Vec3(); + const pA = Vec3(); + const pB = Vec3(); const builderProps = { linkCount: edgeCount, position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { - const { unitA, indexA, unitB, indexB } = edges[edgeIndex]; + const { unitA, indexA, unitB, indexB, props: { type: t } } = edges[edgeIndex]; const fA = unitsFeatures.get(unitA); const fB = unitsFeatures.get(unitB); - const uA = structure.unitMap.get(unitA); - const uB = structure.unitMap.get(unitB); - - Vec3.set(posA, fA.x[indexA], fA.y[indexA], fA.z[indexA]); - Vec3.transformMat4(posA, posA, uA.conformation.operator.matrix); + const uA = structure.unitMap.get(unitA) as Unit.Atomic; + const uB = structure.unitMap.get(unitB) as Unit.Atomic; + + if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && ( + t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond) + ) { + const idxA = fA.members[fA.offsets[indexA]]; + const idxB = fB.members[fB.offsets[indexB]]; + uA.conformation.position(uA.elements[idxA], pA); + uB.conformation.position(uB.elements[idxB], pB); + let minDistA = Vec3.distance(pA, pB); + let minDistB = minDistA; + Vec3.copy(posA, pA); + Vec3.copy(posB, pB); + + eachBondedAtom(structure, uA, idxA, (u, idx) => { + const eI = u.elements[idx]; + if (isHydrogen(structure, u, eI, 'polar')) { + u.conformation.position(eI, p); + const dist = Vec3.distance(p, pB); + if (dist < minDistA) { + minDistA = dist; + Vec3.copy(posA, p); + } + } + }); + + eachBondedAtom(structure, uB, idxB, (u, idx) => { + const eI = u.elements[idx]; + if (isHydrogen(structure, u, eI, 'polar')) { + u.conformation.position(eI, p); + const dist = Vec3.distance(p, pA); + if (dist < minDistB) { + minDistB = dist; + Vec3.copy(posB, p); + } + } + }); + } else { + Vec3.set(posA, fA.x[indexA], fA.y[indexA], fA.z[indexA]); + Vec3.transformMat4(posA, posA, uA.conformation.operator.matrix); - Vec3.set(posB, fB.x[indexB], fB.y[indexB], fB.z[indexB]); - Vec3.transformMat4(posB, posB, uB.conformation.operator.matrix); + Vec3.set(posB, fB.x[indexB], fB.y[indexB], fB.z[indexB]); + Vec3.transformMat4(posB, posB, uB.conformation.operator.matrix); + } }, style: (edgeIndex: number) => LinkStyle.Dashed, radius: (edgeIndex: number) => { @@ -155,6 +197,8 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || newProps.radialSegments !== currentProps.radialSegments || + newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || + newProps.ignoreHydrogensVariant !== currentProps.ignoreHydrogensVariant || newProps.parentDisplay !== currentProps.parentDisplay ); diff --git a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts index e0289f7b75aeffbf37ba01dd2ff02a6194b75172..a21140c29029b0f8df7ce7e6adf744fb956525c0 100644 --- a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts +++ b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -21,9 +21,11 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator'; import { Interactions } from '../interactions/interactions'; import { InteractionFlag } from '../interactions/common'; import { Sphere3D } from '../../../mol-math/geometry'; -import { StructureGroup } from '../../../mol-repr/structure/visual/util/common'; +import { StructureGroup, isHydrogen } from '../../../mol-repr/structure/visual/util/common'; import { assertUnreachable } from '../../../mol-util/type-helpers'; import { InteractionsSharedParams } from './shared'; +import { InteractionType } from '../interactions/common'; +import { eachIntraBondedAtom } from '../chemistry/util'; async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) { if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh); @@ -39,22 +41,64 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: const contacts = interactions.unitsContacts.get(unit.id); const { x, y, z, members, offsets } = features; - const { edgeCount, a, b, edgeProps: { flag } } = contacts; - const { sizeFactor, parentDisplay } = props; + const { edgeCount, a, b, edgeProps: { flag, type } } = contacts; + const { sizeFactor, ignoreHydrogens, ignoreHydrogensVariant, parentDisplay } = props; if (!edgeCount) return Mesh.createEmpty(mesh); + const pos = unit.conformation.invariantPosition; + const { elements } = unit; + const p = Vec3(); + const pA = Vec3(); + const pB = Vec3(); + const builderProps = { linkCount: edgeCount * 2, position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { - Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]); - Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]); + const t = type[edgeIndex]; + if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && ( + t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond) + ) { + const idxA = members[offsets[a[edgeIndex]]]; + const idxB = members[offsets[b[edgeIndex]]]; + pos(elements[idxA], pA); + pos(elements[idxB], pB); + let minDistA = Vec3.distance(pA, pB); + let minDistB = minDistA; + Vec3.copy(posA, pA); + Vec3.copy(posB, pB); + + eachIntraBondedAtom(unit, idxA, (_, idx) => { + if (isHydrogen(structure, unit, elements[idx], 'polar')) { + pos(elements[idx], p); + const dist = Vec3.distance(p, pB); + if (dist < minDistA) { + minDistA = dist; + Vec3.copy(posA, p); + } + } + }); + + eachIntraBondedAtom(unit, idxB, (_, idx) => { + if (isHydrogen(structure, unit, elements[idx], 'polar')) { + pos(elements[idx], p); + const dist = Vec3.distance(p, pA); + if (dist < minDistB) { + minDistB = dist; + Vec3.copy(posB, p); + } + } + }); + } else { + Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]); + Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]); + } }, style: (edgeIndex: number) => LinkStyle.Dashed, radius: (edgeIndex: number) => { - location.element = unit.elements[members[offsets[a[edgeIndex]]]]; + location.element = elements[members[offsets[a[edgeIndex]]]]; const sizeA = theme.size.size(location); - location.element = unit.elements[members[offsets[b[edgeIndex]]]]; + location.element = elements[members[offsets[b[edgeIndex]]]]; const sizeB = theme.size.size(location); return Math.min(sizeA, sizeB) * sizeFactor; }, @@ -65,7 +109,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: if (parentDisplay === 'stub') { const f = a[edgeIndex]; for (let i = offsets[f], il = offsets[f + 1]; i < il; ++i) { - const e = unit.elements[members[offsets[i]]]; + const e = elements[members[offsets[i]]]; if (!SortedArray.has(childUnit.elements, e)) return true; } } else if (parentDisplay === 'full' || parentDisplay === 'between') { @@ -74,13 +118,13 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: const fA = a[edgeIndex]; for (let i = offsets[fA], il = offsets[fA + 1]; i < il; ++i) { - const eA = unit.elements[members[offsets[i]]]; + const eA = elements[members[offsets[i]]]; if (!SortedArray.has(childUnit.elements, eA)) flagA = true; } const fB = b[edgeIndex]; for (let i = offsets[fB], il = offsets[fB + 1]; i < il; ++i) { - const eB = unit.elements[members[offsets[i]]]; + const eB = elements[members[offsets[i]]]; if (!SortedArray.has(childUnit.elements, eB)) flagB = true; } @@ -127,6 +171,8 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || newProps.radialSegments !== currentProps.radialSegments || + newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || + newProps.ignoreHydrogensVariant !== currentProps.ignoreHydrogensVariant || newProps.parentDisplay !== currentProps.parentDisplay ); diff --git a/src/mol-model-props/computed/representations/shared.ts b/src/mol-model-props/computed/representations/shared.ts index 61d70bf0745baf00cd9de2ae558a3e19116e54c1..35193e7ef490fd68cb01bfa3aad6b8b8ed8bdd26 100644 --- a/src/mol-model-props/computed/representations/shared.ts +++ b/src/mol-model-props/computed/representations/shared.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -10,6 +10,8 @@ export const InteractionsSharedParams = { sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }), dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }), dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }), + ignoreHydrogens: PD.Boolean(false), + ignoreHydrogensVariant: PD.Select('all', PD.arrayToOptions(['all', 'non-polar'] as const)), includeParent: PD.Boolean(false), parentDisplay: PD.Select('stub', PD.arrayToOptions(['stub', 'full', 'between'] as const), { description: 'Only has an effect when "includeParent" is enabled. "Stub" shows just the child side of interactions to the parent. "Full" shows both sides of interactions to the parent. "Between" shows only interactions to the parent.' }), }; diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts index 20dd1da691b7a677d4730c8b04fee82e9737293e..8141b0918e2807403277ba3c04840d4977ee6d5e 100644 --- a/src/mol-repr/structure/visual/util/common.ts +++ b/src/mol-repr/structure/visual/util/common.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -314,12 +314,14 @@ export function getStructureConformationAndRadius(structure: Structure, sizeThem } const _H = AtomicNumbers['H']; -export function isHydrogen(structure: Structure, unit: Unit, element: ElementIndex, variant: 'all' | 'non-polar') { +export function isHydrogen(structure: Structure, unit: Unit, element: ElementIndex, variant: 'all' | 'non-polar' | 'polar') { if (Unit.isCoarse(unit)) return false; - return ( - unit.model.atomicHierarchy.derived.atom.atomicNumber[element] === _H && - (variant === 'all' || !hasPolarNeighbour(structure, unit, SortedArray.indexOf(unit.elements, element) as StructureElement.UnitIndex)) - ); + if (unit.model.atomicHierarchy.derived.atom.atomicNumber[element] !== _H) return false; + if (variant === 'all') return true; + const polar = hasPolarNeighbour(structure, unit, SortedArray.indexOf(unit.elements, element) as StructureElement.UnitIndex); + if (polar && variant === 'polar') return true; + if (!polar && variant === 'non-polar') return true; + return false; } export function isH(atomicNumber: ArrayLike<number>, element: ElementIndex) { return atomicNumber[element] === _H;