diff --git a/src/mol-geo/representation/structure/cartoon.ts b/src/mol-geo/representation/structure/cartoon.ts index 5f11c26f8c907b1f81bd5268682f70a832ce0ddc..359709b5d9c8b0a6667124581611a910d8b6b7b6 100644 --- a/src/mol-geo/representation/structure/cartoon.ts +++ b/src/mol-geo/representation/structure/cartoon.ts @@ -12,29 +12,33 @@ import { Loci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../util/marker-data'; import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh'; import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder'; +import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from './visual/nucleotide-block-mesh'; export const DefaultCartoonProps = { ...DefaultPolymerTraceProps, - ...DefaultPolymerGapProps + ...DefaultPolymerGapProps, + ...DefaultNucleotideBlockProps } export type CartoonProps = Partial<typeof DefaultCartoonProps> export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual) const gapRepr = StructureUnitsRepresentation(PolymerGapVisual) + const blockRepr = StructureUnitsRepresentation(NucleotideBlockVisual) return { get renderObjects() { - return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects ] + return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects ] }, get props() { - return { ...traceRepr.props, ...gapRepr.props } + return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props } }, create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => { const p = Object.assign({}, DefaultCartoonProps, props) return Task.create('CartoonRepresentation', async ctx => { await traceRepr.create(structure, p).runInContext(ctx) await gapRepr.create(structure, p).runInContext(ctx) + await blockRepr.create(structure, p).runInContext(ctx) }) }, update: (props: CartoonProps) => { @@ -42,20 +46,26 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { return Task.create('Updating CartoonRepresentation', async ctx => { await traceRepr.update(p).runInContext(ctx) await gapRepr.update(p).runInContext(ctx) + await blockRepr.update(p).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { const traceLoci = traceRepr.getLoci(pickingId) const gapLoci = gapRepr.getLoci(pickingId) - return isEmptyLoci(traceLoci) ? gapLoci : traceLoci + const blockLoci = blockRepr.getLoci(pickingId) + return !isEmptyLoci(traceLoci) ? traceLoci + : !isEmptyLoci(gapLoci) ? gapLoci + : blockLoci }, mark: (loci: Loci, action: MarkerAction) => { traceRepr.mark(loci, action) gapRepr.mark(loci, action) + blockRepr.mark(loci, action) }, destroy() { traceRepr.destroy() gapRepr.destroy() + blockRepr.destroy() } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..1da058ee1609464891fff4dd3020375f5987e859 --- /dev/null +++ b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util/value-cell' + +import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object' +import { Unit } from 'mol-model/structure'; +import { DefaultStructureProps, UnitsVisual } from '..'; +import { RuntimeContext } from 'mol-task' +import { createTransforms, createColors } from './util/common'; +import { deepEqual } from 'mol-util'; +import { MeshValues } from 'mol-gl/renderable'; +import { getMeshData } from '../../../util/mesh-data'; +import { Mesh } from '../../../shape/mesh'; +import { PickingId } from '../../../util/picking'; +import { createMarkers, MarkerAction } from '../../../util/marker-data'; +import { Loci } from 'mol-model/loci'; +import { SizeTheme } from '../../../theme'; +import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util'; +import { MeshBuilder } from '../../../shape/mesh-builder'; +import { getElementLoci, markElement } from './util/element'; +import { Vec3, Mat4 } from 'mol-math/linear-algebra'; +import { Segmentation, SortedArray } from 'mol-data/int'; +import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types'; +import { getElementIndexForAtomId } from 'mol-model/structure/util'; +import { getElementIndexForResidueTypeAtomId } from './util/polymer'; + +async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) { + if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh) + + const builder = MeshBuilder.create(256, 128, mesh) + + const { elements, model } = unit + const { chemicalComponentMap, modifiedResidues } = unit.model.properties + const { chainAtomSegments, residueAtomSegments, residues } = unit.model.atomicHierarchy + const { label_comp_id } = residues + const pos = unit.conformation.invariantPosition + + const chainIt = Segmentation.transientSegments(chainAtomSegments, elements) + const residueIt = Segmentation.transientSegments(residueAtomSegments, elements) + + const p1 = Vec3.zero() + const p2 = Vec3.zero() + const p3 = Vec3.zero() + const p4 = Vec3.zero() + const p5 = Vec3.zero() + const p6 = Vec3.zero() + const v12 = Vec3.zero() + const v34 = Vec3.zero() + const vC = Vec3.zero() + const center = Vec3.zero() + const t = Mat4.identity() + + let i = 0 + while (chainIt.hasNext) { + residueIt.setSegment(chainIt.move()); + + while (residueIt.hasNext) { + const { index: residueIndex } = residueIt.move(); + const cc = chemicalComponentMap.get(label_comp_id.value(residueIndex)) + const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown + + if (isNucleic(moleculeType)) { + let compId = label_comp_id.value(residueIndex) + const parentId = modifiedResidues.parentId.get(compId) + if (parentId !== undefined) compId = parentId + let idx1 = -1, idx2 = -1, idx3 = -1, idx4 = -1, idx5 = -1, idx6 = -1 + let width = 4.5, height = 4.5, depth = 0.5 + + if (isPurinBase(compId)) { + height = 4.5 + idx1 = getElementIndexForAtomId(model, residueIndex, 'N1') + idx2 = getElementIndexForAtomId(model, residueIndex, 'C4') + idx3 = getElementIndexForAtomId(model, residueIndex, 'C6') + idx4 = getElementIndexForAtomId(model, residueIndex, 'C2') + idx5 = getElementIndexForAtomId(model, residueIndex, 'N9') + idx6 = getElementIndexForResidueTypeAtomId(model, residueIndex, 'trace') + } else if (isPyrimidineBase(compId)) { + height = 3.0 + idx1 = getElementIndexForAtomId(model, residueIndex, 'N3') + idx2 = getElementIndexForAtomId(model, residueIndex, 'C6') + idx3 = getElementIndexForAtomId(model, residueIndex, 'C4') + idx4 = getElementIndexForAtomId(model, residueIndex, 'C2') + idx5 = getElementIndexForAtomId(model, residueIndex, 'N1') + idx6 = getElementIndexForResidueTypeAtomId(model, residueIndex, 'trace') + } + + if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1 && idx5 !== -1 && idx6 !== -1) { + pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); pos(idx5, p5); pos(idx6, p6) + Vec3.normalize(v12, Vec3.sub(v12, p2, p1)) + Vec3.normalize(v34, Vec3.sub(v34, p4, p3)) + Vec3.normalize(vC, Vec3.cross(vC, v12, v34)) + Mat4.targetTo(t, p1, p2, vC) + Vec3.scaleAndAdd(center, p1, v12, height / 2) + Mat4.setTranslation(t, center) + builder.setId(SortedArray.findPredecessorIndex(elements, idx6)) + builder.addBox(t, { width: width, height: depth, depth: height }) + builder.addCylinder(p5, p6, 1, { radiusTop: 0.2, radiusBottom: 0.2 }) + } + } + + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Gap mesh', current: i }); + } + ++i + } + } + + return builder.getMesh() +} + +export const DefaultNucleotideBlockProps = { + ...DefaultMeshProps, + ...DefaultStructureProps, + sizeTheme: { name: 'physical', factor: 1 } as SizeTheme, + detail: 0, + unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[] +} +export type NucleotideBlockProps = Partial<typeof DefaultNucleotideBlockProps> + +export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> { + let renderObject: MeshRenderObject + let currentProps: typeof DefaultNucleotideBlockProps + let mesh: Mesh + let currentGroup: Unit.SymmetryGroup + + return { + get renderObject () { return renderObject }, + async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: NucleotideBlockProps = {}) { + currentProps = Object.assign({}, DefaultNucleotideBlockProps, props) + currentGroup = group + + const { colorTheme, unitKinds } = { ...DefaultNucleotideBlockProps, ...props } + const instanceCount = group.units.length + const elementCount = group.elements.length + const unit = group.units[0] + + mesh = unitKinds.includes(unit.kind) + ? await createNucleotideBlockMesh(ctx, unit, mesh) + : Mesh.createEmpty(mesh) + // console.log(mesh) + + const transforms = createTransforms(group) + const color = createColors(group, elementCount, colorTheme) + const marker = createMarkers(instanceCount * elementCount) + + const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount } + + const values: MeshValues = { + ...getMeshData(mesh), + ...color, + ...marker, + aTransform: transforms, + elements: mesh.indexBuffer, + ...createMeshValues(currentProps, counts), + aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3)) + } + const state = createRenderableState(currentProps) + + renderObject = createMeshRenderObject(values, state) + }, + async update(ctx: RuntimeContext, props: NucleotideBlockProps) { + const newProps = Object.assign({}, currentProps, props) + + if (!renderObject) return false + + let updateColor = false + + if (newProps.detail !== currentProps.detail) { + const unit = currentGroup.units[0] + mesh = await createNucleotideBlockMesh(ctx, unit, mesh) + ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) + updateColor = true + } + + if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) { + updateColor = true + } + + if (updateColor) { + const elementCount = currentGroup.elements.length + if (ctx.shouldUpdate) await ctx.update('Computing nucleotide block colors'); + createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values) + } + + updateMeshValues(renderObject.values, newProps) + updateRenderableState(renderObject.state, newProps) + + currentProps = newProps + return true + }, + getLoci(pickingId: PickingId) { + return getElementLoci(renderObject.id, currentGroup, pickingId) + }, + mark(loci: Loci, action: MarkerAction) { + markElement(renderObject.values.tMarker, currentGroup, loci, action) + }, + destroy() { + // TODO + } + } +} diff --git a/src/mol-geo/representation/structure/visual/util/polymer.ts b/src/mol-geo/representation/structure/visual/util/polymer.ts index 394f4d1d952ef645facd1d245376636cd6cddb80..c6ed6ff0e98e9bde968ec536f73ef17bd2aabb6c 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer.ts @@ -11,6 +11,7 @@ import Iterator from 'mol-data/iterator'; import { Vec3 } from 'mol-math/linear-algebra'; import SortedRanges from 'mol-data/int/sorted-ranges'; import { CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse'; +import { getElementIndexForAtomId, getMoleculeType } from 'mol-model/structure/util'; export function getPolymerRanges(unit: Unit): SortedRanges<ElementIndex> { switch (unit.kind) { @@ -67,7 +68,7 @@ export function getPolymerGapCount(unit: Unit) { return count } -function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'direction') { +export function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'direction') { switch (moleculeType) { case MoleculeType.protein: switch (atomType) { @@ -91,35 +92,11 @@ function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'd return '' } -function getMoleculeType(model: Model, residueIndex: ResidueIndex) { - const compId = model.atomicHierarchy.residues.label_comp_id.value(residueIndex) - const chemCompMap = model.properties.chemicalComponentMap - const cc = chemCompMap.get(compId) - return cc ? cc.moleculeType : MoleculeType.unknown -} - -function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex { - const { offsets } = model.atomicHierarchy.residueAtomSegments - const { label_atom_id } = model.atomicHierarchy.atoms - for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) { - if (label_atom_id.value(j) === atomId) return j as ElementIndex - } - return offsets[rI] as ElementIndex -} - -function getElementIndexForResidueTypeAtomId(model: Model, rI: ResidueIndex, atomType: 'trace' | 'direction') { +export function getElementIndexForResidueTypeAtomId(model: Model, rI: ResidueIndex, atomType: 'trace' | 'direction') { const atomId = getResidueTypeAtomId(getMoleculeType(model, rI), atomType) return getElementIndexForAtomId(model, rI, atomId) } -// function residueLabel(model: Model, rI: number) { -// const { residues, chains, residueSegments, chainSegments } = model.atomicHierarchy -// const { label_comp_id, label_seq_id } = residues -// const { label_asym_id } = chains -// const cI = chainSegments.segmentMap[residueSegments.segments[rI]] -// return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}` -// } - /** Iterates over consecutive pairs of residues/coarse elements in polymers */ export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePair> { switch (unit.kind) { diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts index a23c5503950401af6c82307b0cbcbdce23bfbadd..603d486f6944815011150caf3e6829458442b517 100644 --- a/src/mol-math/linear-algebra/3d/mat4.ts +++ b/src/mol-math/linear-algebra/3d/mat4.ts @@ -355,6 +355,24 @@ namespace Mat4 { return out; } + /** + * Sets the specified quaternion with values corresponding to the given + * axes. Each axis is a vec3 and is expected to be unit length and + * perpendicular to all other specified axes. + */ + export function setAxes(out: Mat4, view: Vec3, right: Vec3, up: Vec3) { + out[0] = right[0]; + out[4] = right[1]; + out[8] = right[2]; + out[1] = up[0]; + out[5] = up[1]; + out[9] = up[2]; + out[2] = view[0]; + out[6] = view[1]; + out[10] = view[2]; + return out + } + export function rotate(out: Mat4, a: Mat4, rad: number, axis: Mat4) { let x = axis[0], y = axis[1], z = axis[2], len = Math.sqrt(x * x + y * y + z * z), @@ -747,6 +765,60 @@ namespace Mat4 { return out; } + /** + * Generates a matrix that makes something look at something else. + */ + export function targetTo(out: Mat4, eye: Vec3, target: Vec3, up: Vec3) { + const eyex = eye[0], + eyey = eye[1], + eyez = eye[2], + upx = up[0], + upy = up[1], + upz = up[2]; + + let z0 = eyex - target[0], + z1 = eyey - target[1], + z2 = eyez - target[2]; + + let len = z0*z0 + z1*z1 + z2*z2; + if (len > 0) { + len = 1 / Math.sqrt(len); + z0 *= len; + z1 *= len; + z2 *= len; + } + + let x0 = upy * z2 - upz * z1, + x1 = upz * z0 - upx * z2, + x2 = upx * z1 - upy * z0; + + len = x0*x0 + x1*x1 + x2*x2; + if (len > 0) { + len = 1 / Math.sqrt(len); + x0 *= len; + x1 *= len; + x2 *= len; + } + + out[0] = x0; + out[1] = x1; + out[2] = x2; + out[3] = 0; + out[4] = z1 * x2 - z2 * x1; + out[5] = z2 * x0 - z0 * x2; + out[6] = z0 * x1 - z1 * x0; + out[7] = 0; + out[8] = z0; + out[9] = z1; + out[10] = z2; + out[11] = 0; + out[12] = eyex; + out[13] = eyey; + out[14] = eyez; + out[15] = 1; + return out; + } + /** * Perm is 0-indexed permutation */ diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 841c79a2506d98663b2b96613a3875fde3895d69..cced8c8808d71d137ffbd8b352fd3f2b42a09d7e 100644 --- a/src/mol-model/structure/model/types.ts +++ b/src/mol-model/structure/model/types.ts @@ -108,6 +108,15 @@ export const WaterNames = [ 'SOL', 'WAT', 'HOH', 'H2O', 'W', 'DOD', 'D3O', 'TIP3', 'TIP4', 'SPC' ] +export const RnaBaseNames = [ 'A', 'C', 'T', 'G', 'I', 'U' ] +export const DnaBaseNames = [ 'DA', 'DC', 'DT', 'DG', 'DI', 'DU' ] +export const PurinBaseNames = [ 'A', 'G', 'DA', 'DG', 'DI' ] +export const PyrimidineBaseNames = [ 'C', 'T', 'U', 'DC', 'DT', 'DU' ] +export const BaseNames = RnaBaseNames.concat(DnaBaseNames) + +export const isPurinBase = (compId: string) => PurinBaseNames.includes(compId.toUpperCase()) +export const isPyrimidineBase = (compId: string) => PyrimidineBaseNames.includes(compId.toUpperCase()) + /** get the molecule type from component type and id */ export function getMoleculeType(compType: string, compId: string) { compType = compType.toUpperCase() @@ -133,6 +142,10 @@ export function isPolymer(moleculeType: MoleculeType) { return moleculeType === MoleculeType.protein || moleculeType === MoleculeType.DNA || moleculeType === MoleculeType.RNA } +export function isNucleic(moleculeType: MoleculeType) { + return moleculeType === MoleculeType.DNA || moleculeType === MoleculeType.RNA +} + /** * all chemical components with the word "ion" in their name, Sep 2016 * diff --git a/src/mol-model/structure/util.ts b/src/mol-model/structure/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..eab551c3f9cb9d71e15c42d8e3c05a1b48a15d31 --- /dev/null +++ b/src/mol-model/structure/util.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Model, ResidueIndex, ElementIndex } from './model'; +import { MoleculeType } from './model/types'; + +export function getMoleculeType(model: Model, rI: ResidueIndex) { + const compId = model.atomicHierarchy.residues.label_comp_id.value(rI) + const chemCompMap = model.properties.chemicalComponentMap + const cc = chemCompMap.get(compId) + return cc ? cc.moleculeType : MoleculeType.unknown +} + +export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex { + const { offsets } = model.atomicHierarchy.residueAtomSegments + const { label_atom_id } = model.atomicHierarchy.atoms + for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) { + if (label_atom_id.value(j) === atomId) return j as ElementIndex + } + return offsets[rI] as ElementIndex +} + +export function residueLabel(model: Model, rI: number) { + const { residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy + const { label_comp_id, label_seq_id } = residues + const { label_asym_id } = chains + const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]] + return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}` +} \ No newline at end of file diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 88979afedd7c2cff372a1f60bd88da51d70c63dc..fc98227bffec26de5a236923d7609e8dd3169b6a 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -80,7 +80,14 @@ export class Stage { // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water // this.loadPdbid('4v5a') // ribosome // this.loadPdbid('3j3q') // ... - this.loadPdbid('2np2') // dna + // this.loadPdbid('2np2') // dna + // this.loadPdbid('1d66') // dna + this.loadPdbid('1bna') // B form dna + // this.loadPdbid('1y26') // rna + // this.loadPdbid('1xv6') // rna, modified nucleotides + // this.loadPdbid('3bbm') // rna with linker + // this.loadPdbid('1gfl') // GFP, flourophore has carbonyl oxygen removed + // this.loadPdbid('1sfi') // contains cyclic peptid // this.loadPdbid('3sn6') // discontinuous chains // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)