From f8d7ecb82112449915eab7f4d7e92c284588b86f Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Tue, 17 Jul 2018 19:19:41 -0700 Subject: [PATCH] added gap visual --- src/mol-data/int/sorted-ranges.ts | 7 +- .../representation/structure/cartoon.ts | 19 ++- .../structure/visual/polymer-gap-cylinder.ts | 158 ++++++++++++++++++ .../structure/visual/util/polymer.ts | 96 ++++++++++- .../model/properties/utils/atomic-ranges.ts | 12 +- .../model/properties/utils/coarse-ranges.ts | 2 + src/mol-view/stage.ts | 4 +- 7 files changed, 283 insertions(+), 15 deletions(-) create mode 100644 src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts diff --git a/src/mol-data/int/sorted-ranges.ts b/src/mol-data/int/sorted-ranges.ts index 032f73ff1..362db4124 100644 --- a/src/mol-data/int/sorted-ranges.ts +++ b/src/mol-data/int/sorted-ranges.ts @@ -68,14 +68,17 @@ namespace SortedRanges { } constructor(private ranges: SortedRanges<T>, private set: OrderedSet<T>) { - if (ranges.length) { + // TODO cleanup, refactor to make it clearer + const min = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.min(set)) + const max = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.max(set)) + if (ranges.length && min !== max) { this.curIndex = this.getRangeIndex(OrderedSet.min(set)) this.maxIndex = Math.min(ranges.length - 2, this.getRangeIndex(OrderedSet.max(set))) this.curMin = this.ranges[this.curIndex] this.updateInterval() } - this.hasNext = ranges.length > 0 && this.curIndex <= this.maxIndex + this.hasNext = ranges.length > 0 && min !== max && this.curIndex <= this.maxIndex } } } diff --git a/src/mol-geo/representation/structure/cartoon.ts b/src/mol-geo/representation/structure/cartoon.ts index 445b6f92c..5f11c26f8 100644 --- a/src/mol-geo/representation/structure/cartoon.ts +++ b/src/mol-geo/representation/structure/cartoon.ts @@ -8,45 +8,54 @@ import { StructureRepresentation, StructureUnitsRepresentation } from '.'; import { PickingId } from '../../util/picking'; import { Structure } from 'mol-model/structure'; import { Task } from 'mol-task'; -import { Loci } from 'mol-model/loci'; +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'; export const DefaultCartoonProps = { - ...DefaultPolymerTraceProps + ...DefaultPolymerTraceProps, + ...DefaultPolymerGapProps } export type CartoonProps = Partial<typeof DefaultCartoonProps> export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual) + const gapRepr = StructureUnitsRepresentation(PolymerGapVisual) return { get renderObjects() { - return [ ...traceRepr.renderObjects ] + return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects ] }, get props() { - return { ...traceRepr.props } + return { ...traceRepr.props, ...gapRepr.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) }) }, update: (props: CartoonProps) => { const p = Object.assign({}, props) return Task.create('Updating CartoonRepresentation', async ctx => { await traceRepr.update(p).runInContext(ctx) + await gapRepr.update(p).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { - return traceRepr.getLoci(pickingId) + const traceLoci = traceRepr.getLoci(pickingId) + const gapLoci = gapRepr.getLoci(pickingId) + return isEmptyLoci(traceLoci) ? gapLoci : traceLoci }, mark: (loci: Loci, action: MarkerAction) => { traceRepr.mark(loci, action) + gapRepr.mark(loci, action) }, destroy() { traceRepr.destroy() + gapRepr.destroy() } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts new file mode 100644 index 000000000..6e095c7f1 --- /dev/null +++ b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts @@ -0,0 +1,158 @@ +/** + * 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 { getPolymerGapCount, PolymerGapIterator } from './util/polymer'; +import { getElementLoci, markElement } from './util/element'; +import { Vec3 } from 'mol-math/linear-algebra'; + +async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) { + const polymerGapCount = getPolymerGapCount(unit) + if (!polymerGapCount) return Mesh.createEmpty(mesh) + console.log('polymerGapCount', polymerGapCount) + + // TODO better vertex count estimates + const builder = MeshBuilder.create(polymerGapCount * 30, polymerGapCount * 30 / 2, mesh) + + const { elements } = unit + const pos = unit.conformation.invariantPosition + const pA = Vec3.zero() + const pB = Vec3.zero() + + let i = 0 + const polymerGapIt = PolymerGapIterator(unit) + while (polymerGapIt.hasNext) { + // TODO size theme + const { centerA, centerB } = polymerGapIt.move() + if (centerA.element === centerB.element) { + builder.setId(centerA.element) + pos(elements[centerA.element], pA) + builder.addIcosahedron(pA, 0.6, 0) + } else { + pos(elements[centerA.element], pA) + pos(elements[centerB.element], pB) + builder.setId(centerA.element) + builder.addFixedCountDashedCylinder(pA, pB, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 }) + builder.setId(centerB.element) + builder.addFixedCountDashedCylinder(pB, pA, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 }) + } + + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount }); + } + ++i + } + + return builder.getMesh() +} + +export const DefaultPolymerGapProps = { + ...DefaultMeshProps, + ...DefaultStructureProps, + sizeTheme: { name: 'physical', factor: 1 } as SizeTheme, + detail: 0, + unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[] +} +export type PolymerGapProps = Partial<typeof DefaultPolymerGapProps> + +export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> { + let renderObject: MeshRenderObject + let currentProps: typeof DefaultPolymerGapProps + let mesh: Mesh + let currentGroup: Unit.SymmetryGroup + + return { + get renderObject () { return renderObject }, + async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerGapProps = {}) { + currentProps = Object.assign({}, DefaultPolymerGapProps, props) + currentGroup = group + + const { colorTheme, unitKinds } = { ...DefaultPolymerGapProps, ...props } + const instanceCount = group.units.length + const elementCount = group.elements.length + const unit = group.units[0] + + mesh = unitKinds.includes(unit.kind) + ? await createPolymerGapCylinderMesh(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: PolymerGapProps) { + 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 createPolymerGapCylinderMesh(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 trace 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 0739cd7c9..149443278 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer.ts @@ -20,6 +20,14 @@ export function getPolymerRanges(unit: Unit): SortedRanges<ElementIndex> { } } +export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> { + switch (unit.kind) { + case Unit.Kind.Atomic: return unit.model.atomicHierarchy.gapRanges + case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.gapRanges + case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.gapRanges + } +} + export function getPolymerElementCount(unit: Unit) { let count = 0 const { elements } = unit @@ -48,6 +56,17 @@ export function getPolymerElementCount(unit: Unit) { return count } +export function getPolymerGapCount(unit: Unit) { + let count = 0 + const { elements } = unit + const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements) + while (gapIt.hasNext) { + const { start, end } = gapIt.move() + if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count + } + return count +} + function getResidueTypeAtomId(moleculeType: MoleculeType, atomType: 'trace' | 'direction') { switch (moleculeType) { case MoleculeType.protein: @@ -134,9 +153,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') { const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType) - // // TODO handle case when it returns -1 - // return SortedArray.indexOf(this.unit.elements, index) as ElementIndex - + // TODO handle case when it returns -1 const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex if (elementIndex === -1) { console.log('-1', residueIndex, atomType, index) @@ -222,6 +239,79 @@ export class CoarsePolymerBackboneIterator implements Iterator<PolymerBackbonePa } } +/** Iterates over gaps, i.e. the stem residues/coarse elements adjacent to gaps */ +export function PolymerGapIterator(unit: Unit): Iterator<PolymerGapPair> { + switch (unit.kind) { + case Unit.Kind.Atomic: return new AtomicPolymerGapIterator(unit) + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return new CoarsePolymerGapIterator(unit) + } +} + +interface PolymerGapPair { + centerA: StructureElement + centerB: StructureElement +} + +function createPolymerGapPair (unit: Unit) { + return { + centerA: StructureElement.create(unit), + centerB: StructureElement.create(unit), + } +} + +export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> { + private value: PolymerGapPair + private gapIt: SortedRanges.Iterator<ElementIndex, ResidueIndex> + hasNext: boolean = false; + + private getElementIndex(residueIndex: ResidueIndex, atomType: 'trace' | 'direction') { + const index = getElementIndexForResidueTypeAtomId(this.unit.model, residueIndex, atomType) + // TODO handle case when it returns -1 + const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex + if (elementIndex === -1) { + console.log('-1', residueIndex, atomType, index) + } + return elementIndex === -1 ? 0 as ElementIndex : elementIndex + } + + move() { + const { elements, residueIndex } = this.unit + const gapSegment = this.gapIt.move(); + this.value.centerA.element = this.getElementIndex(residueIndex[elements[gapSegment.start]], 'trace') + this.value.centerB.element = this.getElementIndex(residueIndex[elements[gapSegment.end - 1]], 'trace') + this.hasNext = this.gapIt.hasNext + return this.value; + } + + constructor(private unit: Unit.Atomic) { + this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements); + this.value = createPolymerGapPair(unit) + this.hasNext = this.gapIt.hasNext + } +} + +export class CoarsePolymerGapIterator implements Iterator<PolymerGapPair> { + private value: PolymerGapPair + private gapIt: SortedRanges.Iterator<ElementIndex, ElementIndex> + hasNext: boolean = false; + + move() { + const gapSegment = this.gapIt.move(); + this.value.centerA.element = this.unit.elements[gapSegment.start] + this.value.centerB.element = this.unit.elements[gapSegment.end - 1] + this.hasNext = this.gapIt.hasNext + return this.value; + } + + constructor(private unit: Unit.Spheres | Unit.Gaussians) { + this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements); + this.value = createPolymerGapPair(unit) + this.hasNext = this.gapIt.hasNext + } +} + /** * Iterates over individual residues/coarse elements in polymers of a unit while * providing information about the neighbourhood in the underlying model for drawing splines diff --git a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts index 73d705373..cf4134d08 100644 --- a/src/mol-model/structure/model/properties/utils/atomic-ranges.ts +++ b/src/mol-model/structure/model/properties/utils/atomic-ranges.ts @@ -12,6 +12,8 @@ import { ChemicalComponent } from '../chemical-component'; import { MoleculeType, isPolymer } from '../../types'; import { ElementIndex } from '../../indexing'; +// TODO add gaps at the ends of the chains by comparing to the polymer sequence data + export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chemicalComponentMap: Map<string, ChemicalComponent>): AtomicRanges { const polymerRanges: number[] = [] const gapRanges: number[] = [] @@ -20,6 +22,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem const { label_seq_id, label_comp_id } = data.residues let prevSeqId: number + let prevStart: number let prevEnd: number let startIndex: number @@ -27,6 +30,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem const chainSegment = chainIt.move(); residueIt.setSegment(chainSegment); prevSeqId = -1 + prevStart = -1 prevEnd = -1 startIndex = -1 @@ -40,7 +44,7 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem if (startIndex !== -1) { if (seqId !== prevSeqId + 1) { polymerRanges.push(startIndex, prevEnd - 1) - gapRanges.push(prevEnd - 1, residueSegment.start) + gapRanges.push(prevStart, residueSegment.end - 1) startIndex = residueSegment.start } else if (!residueIt.hasNext) { polymerRanges.push(startIndex, residueSegment.end - 1) @@ -54,13 +58,15 @@ export function getAtomicRanges(data: AtomicData, segments: AtomicSegments, chem startIndex = -1 } } - + + prevStart = residueSegment.start prevEnd = residueSegment.end prevSeqId = seqId } } - console.log(polymerRanges, gapRanges) + console.log('polymerRanges', polymerRanges) + console.log('gapRanges', gapRanges) return { polymerRanges: SortedRanges.ofSortedRanges(polymerRanges as ElementIndex[]), diff --git a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts index 4e3cc0101..1621dffa1 100644 --- a/src/mol-model/structure/model/properties/utils/coarse-ranges.ts +++ b/src/mol-model/structure/model/properties/utils/coarse-ranges.ts @@ -11,6 +11,7 @@ import { ChemicalComponent } from '../chemical-component'; import { ElementIndex } from '../../indexing'; // TODO assumes all coarse elements are part of a polymer +// TODO add gaps at the ends of the chains by comparing to the polymer sequence data export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: Map<string, ChemicalComponent>): CoarseRanges { const polymerRanges: number[] = [] @@ -33,6 +34,7 @@ export function getCoarseRanges(data: CoarseElementData, chemicalComponentMap: M } else { if (prevSeqEnd !== seq_id_begin.value(i) - 1) { polymerRanges.push(startIndex, i - 1) + gapRanges.push(i - 1, i) startIndex = i } } diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 6cef930e5..6c06fbb4f 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -80,10 +80,10 @@ export class Stage { // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water // this.loadPdbid('4v5a') // ribosome // this.loadPdbid('3j3q') // ... - // this.loadPdbid('3sn6') // discontinuous chains + this.loadPdbid('3sn6') // discontinuous chains // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`) - this.loadMmcifUrl(`../../examples/1crn.cif`) + // this.loadMmcifUrl(`../../examples/1crn.cif`) // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000002.cif`) // ok -- GitLab