diff --git a/src/mol-model/structure/structure/carbohydrates/compute.ts b/src/mol-model/structure/structure/carbohydrates/compute.ts index 729e7bb56c1b38207f8297e19a457212eec0cd27..d86ed125b4f6d796b02453c432a0017ebb9a89fc 100644 --- a/src/mol-model/structure/structure/carbohydrates/compute.ts +++ b/src/mol-model/structure/structure/carbohydrates/compute.ts @@ -262,8 +262,9 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { } - // get carbohydrate links induced by inter-unit bonds - // (e.g. for structures from the PDB archive __before__ carbohydrate remediation) + // get carbohydrate links induced by inter-unit bonds, that is + // terminal links plus inter monosaccharide links for structures from the + // PDB archive __before__ carbohydrate remediation for (let i = 0, il = structure.units.length; i < il; ++i) { const unit = structure.units[i] if (!Unit.isAtomic(unit)) continue @@ -309,10 +310,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { }) } - return { links, terminalLinks, elements, partialElements, ...buildLookups(elements, links) } + return { links, terminalLinks, elements, partialElements, ...buildLookups(elements, links, terminalLinks) } } -function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[]) { +function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[], terminalLinks: CarbohydrateTerminalLink[]) { // element lookup function elementKey(unit: Unit, anomericCarbon: ElementIndex) { @@ -347,6 +348,27 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[ return linkMap.get(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB)) } + // terminal link lookup + + function terminalLinkKey(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) { + return `${unitA.id}|${elementA}|${unitB.id}|${elementB}` + } + + const terminalLinkMap = new Map<string, number>() + for (let i = 0, il = terminalLinks.length; i < il; ++i) { + const { fromCarbohydrate, carbohydrateIndex, elementUnit, elementIndex } = terminalLinks[i] + const { unit, anomericCarbon } = elements[carbohydrateIndex] + if (fromCarbohydrate) { + terminalLinkMap.set(terminalLinkKey(unit, anomericCarbon, elementUnit, elementUnit.elements[elementIndex]), i) + } else { + terminalLinkMap.set(terminalLinkKey(elementUnit, elementUnit.elements[elementIndex], unit, anomericCarbon), i) + } + } + + function getTerminalLinkIndex(unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) { + return terminalLinkMap.get(terminalLinkKey(unitA, elementA, unitB, elementB)) + } + // anomeric carbon lookup function anomericCarbonKey(unit: Unit, residueIndex: ResidueIndex) { @@ -364,5 +386,5 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[ return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex)) } - return { getElementIndex, getLinkIndex, getAnomericCarbon } + return { getElementIndex, getLinkIndex, getTerminalLinkIndex, getAnomericCarbon } } \ No newline at end of file diff --git a/src/mol-model/structure/structure/carbohydrates/data.ts b/src/mol-model/structure/structure/carbohydrates/data.ts index 0a8d383755bac79c9ffde4be7de61a4b8f331cf0..7743bd0ba2ab580cd9a5226c07bd0678253953af 100644 --- a/src/mol-model/structure/structure/carbohydrates/data.ts +++ b/src/mol-model/structure/structure/carbohydrates/data.ts @@ -8,6 +8,7 @@ import Unit from '../unit'; import { Vec3 } from 'mol-math/linear-algebra'; import { ResidueIndex, ElementIndex } from '../../model'; import { SaccharideComponent } from './constants'; +import StructureElement from '../element'; export interface CarbohydrateLink { readonly carbohydrateIndexA: number @@ -16,7 +17,7 @@ export interface CarbohydrateLink { export interface CarbohydrateTerminalLink { readonly carbohydrateIndex: number - readonly elementIndex: number + readonly elementIndex: StructureElement.UnitIndex readonly elementUnit: Unit /** specifies direction of the link */ readonly fromCarbohydrate: boolean @@ -31,7 +32,7 @@ export interface CarbohydrateElement { readonly ringAltId: string, } -// partial carbohydrate with no ring present +/** partial carbohydrate with no ring present */ export interface PartialCarbohydrateElement { readonly unit: Unit.Atomic, readonly residueIndex: ResidueIndex, @@ -45,5 +46,6 @@ export interface Carbohydrates { partialElements: ReadonlyArray<PartialCarbohydrateElement> getElementIndex: (unit: Unit, anomericCarbon: ElementIndex) => number | undefined getLinkIndex: (unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) => number | undefined + getTerminalLinkIndex: (unitA: Unit, elementA: ElementIndex, unitB: Unit, elementB: ElementIndex) => number | undefined getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined } \ No newline at end of file diff --git a/src/mol-repr/structure/representation/carbohydrate.ts b/src/mol-repr/structure/representation/carbohydrate.ts index 82660da3fb1f0eba58ad787a8843cca7684450bb..ac2e0d87bd53066ddf807ea3b39c2c2f73c3cc2f 100644 --- a/src/mol-repr/structure/representation/carbohydrate.ts +++ b/src/mol-repr/structure/representation/carbohydrate.ts @@ -6,6 +6,7 @@ import { CarbohydrateSymbolVisual, CarbohydrateSymbolParams } from '../visual/carbohydrate-symbol-mesh'; import { CarbohydrateLinkVisual, CarbohydrateLinkParams } from '../visual/carbohydrate-link-cylinder'; +import { CarbohydrateTerminalLinkParams, CarbohydrateTerminalLinkVisual } from '../visual/carbohydrate-terminal-link-cylinder'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ComplexRepresentation } from '../complex-representation'; import { StructureRepresentation, StructureRepresentationProvider } from '../representation'; @@ -17,6 +18,7 @@ import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/ const CarbohydrateVisuals = { 'carbohydrate-symbol': (getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', getParams, CarbohydrateSymbolVisual), 'carbohydrate-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', getParams, CarbohydrateLinkVisual), + 'carbohydrate-terminal-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', getParams, CarbohydrateTerminalLinkVisual), } type CarbohydrateVisualName = keyof typeof CarbohydrateVisuals const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [name, name] as [CarbohydrateVisualName, string]) @@ -24,8 +26,9 @@ const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [ export const CarbohydrateParams = { ...CarbohydrateSymbolParams, ...CarbohydrateLinkParams, + ...CarbohydrateTerminalLinkParams, colorTheme: PD.Mapped('carbohydrate-symbol', BuiltInColorThemeOptions, getBuiltInColorThemeParams), - visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link'], CarbohydrateVisualOptions), + visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link', 'carbohydrate-terminal-link'], CarbohydrateVisualOptions), } PD.getDefaultValues(CarbohydrateParams).colorTheme.name export type CarbohydrateParams = typeof CarbohydrateParams diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts index e58322f57d5b6f1eb90db7832db26dbe61007433..05d5a04b0d66afd2c4bf649fb66826835538fe13 100644 --- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts @@ -7,7 +7,7 @@ import { Structure, Link, StructureElement } from 'mol-model/structure'; import { Loci, EmptyLoci } from 'mol-model/loci'; import { Vec3 } from 'mol-math/linear-algebra'; -import { createLinkCylinderMesh, LinkCylinderProps, LinkCylinderParams } from './util/link'; +import { createLinkCylinderMesh, LinkCylinderParams } from './util/link'; import { OrderedSet, Interval } from 'mol-data/int'; import { ComplexMeshVisual, ComplexVisual } from '../complex-visual'; import { LinkType } from 'mol-model/structure/model/types'; @@ -21,22 +21,10 @@ import { VisualUpdateState } from '../../util'; import { VisualContext } from 'mol-repr/representation'; import { Theme } from 'mol-theme/theme'; -// TODO create seperate visual -// for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) { -// const tl = carbohydrates.terminalLinks[i] -// const center = carbohydrates.elements[tl.carbohydrateIndex].geometry.center -// tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p) -// if (tl.fromCarbohydrate) { -// builder.addCylinder(center, p, 0.5, linkParams) -// } else { -// builder.addCylinder(p, center, 0.5, linkParams) -// } -// } - -const radiusFactor = 0.3 - -async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: LinkCylinderProps, mesh?: Mesh) { +async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateLinkParams>, mesh?: Mesh) { const { links, elements } = structure.carbohydrates + const { linkSizeFactor } = props + const location = StructureElement.create() const builderProps = { @@ -53,7 +41,7 @@ async function createCarbohydrateLinkCylinderMesh(ctx: VisualContext, structure: const l = links[edgeIndex] location.unit = elements[l.carbohydrateIndexA].unit location.element = elements[l.carbohydrateIndexA].anomericCarbon - return theme.size.size(location) * radiusFactor + return theme.size.size(location) * linkSizeFactor } } @@ -64,6 +52,7 @@ export const CarbohydrateLinkParams = { ...UnitsMeshParams, ...LinkCylinderParams, detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }), + linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }), } export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts index 440d6e834e948a8b2ae6de8cdf85855686316368..7a5acbf598fc3d6b1241632712d95c97ee30358e 100644 --- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts @@ -30,8 +30,7 @@ const t = Mat4.identity() const sVec = Vec3.zero() const pd = Vec3.zero() -const sideFactor = 1.75 * 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4) -const radiusFactor = 1.75 +const SideFactor = 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4) const box = Box() const perforatedBox = PerforatedBox() @@ -47,7 +46,7 @@ const hexagonalPrism = HexagonalPrism() async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateSymbolParams>, mesh?: Mesh) { const builder = MeshBuilder.create(256, 128, mesh) - const { detail } = props + const { detail, sizeFactor } = props const carbohydrates = structure.carbohydrates const n = carbohydrates.elements.length @@ -60,8 +59,8 @@ async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Struc l.unit = c.unit l.element = c.unit.elements[c.anomericCarbon] const size = theme.size.size(l) - const radius = size * radiusFactor - const side = size * sideFactor + const radius = size * sizeFactor + const side = size * sizeFactor * SideFactor const { center, normal, direction } = c.geometry Vec3.add(pd, center, direction) @@ -148,6 +147,7 @@ async function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Struc export const CarbohydrateSymbolParams = { ...ComplexMeshParams, detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }), + sizeFactor: PD.Numeric(1.75, { min: 0, max: 10, step: 0.01 }), } export type CarbohydrateSymbolParams = typeof CarbohydrateSymbolParams diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d594fd788a192d1fa5fb1df4e2ec6f2bff9ca65 --- /dev/null +++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Structure, Link, StructureElement } from 'mol-model/structure'; +import { Loci, EmptyLoci } from 'mol-model/loci'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { createLinkCylinderMesh, LinkCylinderParams } from './util/link'; +import { OrderedSet, Interval } from 'mol-data/int'; +import { ComplexMeshVisual, ComplexVisual } from '../complex-visual'; +import { LinkType } from 'mol-model/structure/model/types'; +import { BitFlags } from 'mol-util'; +import { UnitsMeshParams } from '../units-visual'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { Mesh } from 'mol-geo/geometry/mesh/mesh'; +import { LocationIterator } from 'mol-geo/util/location-iterator'; +import { PickingId } from 'mol-geo/geometry/picking'; +import { VisualUpdateState } from '../../util'; +import { VisualContext } from 'mol-repr/representation'; +import { Theme } from 'mol-theme/theme'; + +async function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateTerminalLinkParams>, mesh?: Mesh) { + const { terminalLinks, elements } = structure.carbohydrates + const { linkSizeFactor } = props + + const location = StructureElement.create() + + const builderProps = { + linkCount: terminalLinks.length, + referencePosition: (edgeIndex: number) => null, + position: (posA: Vec3, posB: Vec3, edgeIndex: number) => { + const l = terminalLinks[edgeIndex] + if (l.fromCarbohydrate) { + Vec3.copy(posA, elements[l.carbohydrateIndex].geometry.center) + l.elementUnit.conformation.position(l.elementIndex, posB) + } else { + l.elementUnit.conformation.position(l.elementIndex, posA) + Vec3.copy(posB, elements[l.carbohydrateIndex].geometry.center) + } + }, + order: (edgeIndex: number) => 1, + flags: (edgeIndex: number) => BitFlags.create(LinkType.Flag.None), + radius: (edgeIndex: number) => { + const l = terminalLinks[edgeIndex] + if (l.fromCarbohydrate) { + location.unit = elements[l.carbohydrateIndex].unit + location.element = elements[l.carbohydrateIndex].anomericCarbon + } else { + location.unit = l.elementUnit + location.element = l.elementUnit.elements[l.elementIndex] + } + return theme.size.size(location) * linkSizeFactor + } + } + + return createLinkCylinderMesh(ctx, builderProps, props, mesh) +} + +export const CarbohydrateTerminalLinkParams = { + ...UnitsMeshParams, + ...LinkCylinderParams, + detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }), + linkSizeFactor: PD.Numeric(0.3, { min: 0, max: 3, step: 0.01 }), +} +export type CarbohydrateTerminalLinkParams = typeof CarbohydrateTerminalLinkParams + +export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerminalLinkParams> { + return ComplexMeshVisual<CarbohydrateTerminalLinkParams>({ + defaultProps: PD.getDefaultValues(CarbohydrateTerminalLinkParams), + createGeometry: createCarbohydrateTerminalLinkCylinderMesh, + createLocationIterator: CarbohydrateTerminalLinkIterator, + getLoci: getTerminalLinkLoci, + mark: markTerminalLink, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CarbohydrateTerminalLinkParams>, currentProps: PD.Values<CarbohydrateTerminalLinkParams>) => { + state.createGeometry = newProps.radialSegments !== currentProps.radialSegments + } + }) +} + +function CarbohydrateTerminalLinkIterator(structure: Structure): LocationIterator { + const { elements, terminalLinks } = structure.carbohydrates + const groupCount = terminalLinks.length + const instanceCount = 1 + const location = Link.Location() + const getLocation = (groupIndex: number) => { + const terminalLink = terminalLinks[groupIndex] + const carb = elements[terminalLink.carbohydrateIndex] + const indexCarb = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon) + if (terminalLink.fromCarbohydrate) { + location.aUnit = carb.unit + location.aIndex = indexCarb as StructureElement.UnitIndex + location.bUnit = terminalLink.elementUnit + location.bIndex = terminalLink.elementIndex + } else { + location.aUnit = terminalLink.elementUnit + location.aIndex = terminalLink.elementIndex + location.bUnit = carb.unit + location.bIndex = indexCarb as StructureElement.UnitIndex + } + return location + } + return LocationIterator(groupCount, instanceCount, getLocation, true) +} + +function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: number) { + const { objectId, groupId } = pickingId + if (id === objectId) { + const { terminalLinks, elements } = structure.carbohydrates + const l = terminalLinks[groupId] + const carb = elements[l.carbohydrateIndex] + const carbIndex = OrderedSet.indexOf(carb.unit.elements, carb.anomericCarbon) + + if (l.fromCarbohydrate) { + return Link.Loci(structure, [ + Link.Location( + carb.unit, carbIndex as StructureElement.UnitIndex, + l.elementUnit, l.elementIndex + ) + ]) + } else { + return Link.Loci(structure, [ + Link.Location( + l.elementUnit, l.elementIndex, + carb.unit, carbIndex as StructureElement.UnitIndex + ) + ]) + } + } + return EmptyLoci +} + +function markTerminalLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) { + const { getTerminalLinkIndex } = structure.carbohydrates + + let changed = false + if (Link.isLoci(loci)) { + for (const l of loci.links) { + const idx = getTerminalLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex]) + if (idx !== undefined) { + if (apply(Interval.ofSingleton(idx))) changed = true + } + } + } + return changed +} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts index 31e82c69c78b9f1e5bad8b5022b02b3a2f7e7e8c..f85233d3541260ded2037f1c37c814e17e00be24 100644 --- a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts +++ b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts @@ -7,7 +7,7 @@ import { Link, Structure, StructureElement } from 'mol-model/structure'; import { ComplexVisual } from '../representation'; import { VisualUpdateState } from '../../util'; -import { LinkCylinderProps, createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link'; +import { createLinkCylinderMesh, LinkIterator, LinkCylinderParams } from './util/link'; import { Vec3 } from 'mol-math/linear-algebra'; import { Loci, EmptyLoci } from 'mol-model/loci'; import { ComplexMeshVisual, ComplexMeshParams } from '../complex-visual'; @@ -19,9 +19,10 @@ import { PickingId } from 'mol-geo/geometry/picking'; import { VisualContext } from 'mol-repr/representation'; import { Theme } from 'mol-theme/theme'; -async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: LinkCylinderProps, mesh?: Mesh) { +async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitLinkParams>, mesh?: Mesh) { const links = structure.links const { bondCount, bonds } = links + const { sizeFactor } = props if (!bondCount) return Mesh.createEmpty(mesh) @@ -42,7 +43,7 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St const b = bonds[edgeIndex] location.unit = b.unitA location.element = b.unitA.elements[b.indexA] - return theme.size.size(location) + return theme.size.size(location) * sizeFactor } } @@ -52,6 +53,7 @@ async function createInterUnitLinkCylinderMesh(ctx: VisualContext, structure: St export const InterUnitLinkParams = { ...ComplexMeshParams, ...LinkCylinderParams, + sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }), } export type InterUnitLinkParams = typeof InterUnitLinkParams