Bladeren bron

added gap visual

Alexander Rose 6 jaren geleden
bovenliggende
commit
f8d7ecb821

+ 5 - 2
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
         }
     }
 }

+ 14 - 5
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()
         }
     }
 }

+ 158 - 0
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
+        }
+    }
+}

+ 93 - 3
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

+ 9 - 3
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[]),

+ 2 - 0
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
                 }
             }

+ 2 - 2
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