Browse Source

wip, inter unit bonds

Alexander Rose 6 years ago
parent
commit
66dc6fbb60

+ 43 - 27
src/apps/structure-info/model.ts

@@ -8,10 +8,8 @@
 import * as argparse from 'argparse'
 require('util.promisify').shim();
 
-// import { Table } from 'mol-data/db'
 import { CifFrame } from 'mol-io/reader/cif'
 import { Model, Structure, Element, Unit, Queries, Format } from 'mol-model/structure'
-// import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
 import { Table } from 'mol-data/db';
 import { openCif, downloadCif } from './helpers';
@@ -79,12 +77,12 @@ export function printLinks(structure: Structure, showIntra: boolean, showInter:
             if (!Unit.isAtomic(unit)) continue;
 
             const elements = unit.elements;
-            const { a, b } = unit.links;
+            const { a, b, edgeCount } = unit.links;
             const { model } = unit;
 
-            if (!a.length) continue;
+            if (!edgeCount) continue;
 
-            for (let bI = 0, _bI = a.length; bI < _bI; bI++) {
+            for (let bI = 0, _bI = edgeCount * 2; bI < _bI; bI++) {
                 const x = a[bI], y = b[bI];
                 if (x >= y) continue;
                 console.log(`${atomLabel(model, elements[x])} -- ${atomLabel(model, elements[y])}`);
@@ -199,44 +197,62 @@ export function printIHMModels(model: Model) {
     console.log(Table.formatToString(model.coarseHierarchy.models));
 }
 
-async function run(frame: CifFrame) {
+async function run(frame: CifFrame, args: Args) {
     const models = await Model.create(Format.mmCIF(frame)).run();
     const structure = Structure.ofModel(models[0]);
-    //printSequence(models[0]);
-    //printIHMModels(models[0]);
-    printUnits(structure);
-    printSymmetryInfo(models[0]);
-    //printRings(structure);
-    //printLinks(structure, true, true);
-    //printModRes(models[0]);
-    //printSecStructure(models[0]);
+
+    if (args.seq) printSequence(models[0]);
+    if (args.ihm) printIHMModels(models[0]);
+    if (args.units) printUnits(structure);
+    if (args.sym) printSymmetryInfo(models[0]);
+    if (args.rings) printRings(structure);
+    if (args.intraLinks) printLinks(structure, true, false);
+    if (args.interLinks) printLinks(structure, false, true);
+    if (args.mod) printModRes(models[0]);
+    if (args.sec) printSecStructure(models[0]);
 }
 
-async function runDL(pdb: string) {
+async function runDL(pdb: string, args: Args) {
     const mmcif = await downloadFromPdb(pdb)
-    run(mmcif);
+    run(mmcif, args);
 }
 
-async function runFile(filename: string) {
+async function runFile(filename: string, args: Args) {
     const mmcif = await readPdbFile(filename);
-    run(mmcif);
+    run(mmcif, args);
 }
 
 const parser = new argparse.ArgumentParser({
     addHelp: true,
     description: 'Print info about a structure, mainly to test and showcase the mol-model module'
 });
-parser.addArgument(['--download', '-d'], {
-    help: 'Pdb entry id'
-});
-parser.addArgument(['--file', '-f'], {
-    help: 'filename'
-});
+parser.addArgument(['--download', '-d'], { help: 'Pdb entry id' });
+parser.addArgument(['--file', '-f'], { help: 'filename' });
+
+parser.addArgument(['--seq'], { help: 'print sequence', action: 'storeTrue' });
+parser.addArgument(['--ihm'], { help: 'print IHM', action: 'storeTrue' });
+parser.addArgument(['--units'], { help: 'print units', action: 'storeTrue' });
+parser.addArgument(['--sym'], { help: 'print symmetry', action: 'storeTrue' });
+parser.addArgument(['--rings'], { help: 'print rings', action: 'storeTrue' });
+parser.addArgument(['--intraLinks'], { help: 'print intra unit links', action: 'storeTrue' });
+parser.addArgument(['--interLinks'], { help: 'print inter unit links', action: 'storeTrue' });
+parser.addArgument(['--mod'], { help: 'print modified residues', action: 'storeTrue' });
+parser.addArgument(['--sec'], { help: 'print secoundary structure', action: 'storeTrue' });
 interface Args {
     download?: string,
-    file?: string
+    file?: string,
+
+    seq?: boolean,
+    ihm?: boolean,
+    units?: boolean,
+    sym?: boolean,
+    rings?: boolean,
+    intraLinks?: boolean,
+    interLinks?: boolean,
+    mod?: boolean,
+    sec?: boolean,
 }
 const args: Args = parser.parseArgs();
 
-if (args.download) runDL(args.download)
-else if (args.file) runFile(args.file)
+if (args.download) runDL(args.download, args)
+else if (args.file) runFile(args.file, args)

+ 14 - 13
src/mol-geo/representation/structure/ball-and-stick.ts

@@ -13,6 +13,7 @@ import { Task } from 'mol-task';
 import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
 import { SizeTheme } from '../../theme';
+import { InterUnitLinkVisual } from './visual/inter-unit-link-cylinder';
 
 export const DefaultBallAndStickProps = {
     ...DefaultElementSphereProps,
@@ -23,29 +24,29 @@ export const DefaultBallAndStickProps = {
 export type BallAndStickProps = Partial<typeof DefaultBallAndStickProps>
 
 export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> {
-    const sphereRepr = StructureRepresentation(ElementSphereVisual)
-    const intraLinkRepr = StructureRepresentation(IntraUnitLinkVisual)
+    const elmementRepr = StructureRepresentation(ElementSphereVisual)
+    const linkRepr = StructureRepresentation(IntraUnitLinkVisual, InterUnitLinkVisual)
 
     return {
         get renderObjects() {
-            return [ ...sphereRepr.renderObjects, ...intraLinkRepr.renderObjects ]
+            return [ ...elmementRepr.renderObjects, ...linkRepr.renderObjects ]
         },
         create: (structure: Structure, props: BallAndStickProps = {} as BallAndStickProps) => {
             const p = Object.assign({}, DefaultBallAndStickProps, props)
             return Task.create('Creating BallAndStickRepresentation', async ctx => {
-                await sphereRepr.create(structure, p).runInContext(ctx)
-                await intraLinkRepr.create(structure, p).runInContext(ctx)
+                await elmementRepr.create(structure, p).runInContext(ctx)
+                await linkRepr.create(structure, p).runInContext(ctx)
             })
         },
         update: (props: BallAndStickProps) => {
             return Task.create('Updating BallAndStickRepresentation', async ctx => {
-                await sphereRepr.update(props).runInContext(ctx)
-                await intraLinkRepr.update(props).runInContext(ctx)
+                await elmementRepr.update(props).runInContext(ctx)
+                await linkRepr.update(props).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
-            const sphereLoci = sphereRepr.getLoci(pickingId)
-            const intraLinkLoci = intraLinkRepr.getLoci(pickingId)
+            const sphereLoci = elmementRepr.getLoci(pickingId)
+            const intraLinkLoci = linkRepr.getLoci(pickingId)
             if (isEmptyLoci(sphereLoci)) {
                 return intraLinkLoci
             } else {
@@ -53,12 +54,12 @@ export function BallAndStickRepresentation(): StructureRepresentation<BallAndSti
             }
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            sphereRepr.mark(loci, action)
-            intraLinkRepr.mark(loci, action)
+            elmementRepr.mark(loci, action)
+            linkRepr.mark(loci, action)
         },
         destroy() {
-            sphereRepr.destroy()
-            intraLinkRepr.destroy()
+            elmementRepr.destroy()
+            linkRepr.destroy()
         }
     }
 }

+ 11 - 1
src/mol-geo/representation/structure/index.ts

@@ -79,7 +79,7 @@ export function StructureRepresentation<P extends StructureProps>(unitsVisualCto
                             unitsVisuals.set(group.hashCode, { visual, group })
                         }
                     }
-    
+
                     // for new groups, reuse leftover visuals
                     const unusedVisuals: UnitsVisual<P>[] = []
                     oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
@@ -125,22 +125,32 @@ export function StructureRepresentation<P extends StructureProps>(unitsVisualCto
             const _loci = visual.getLoci(pickingId)
             if (!isEmptyLoci(_loci)) loci = _loci
         })
+        if (structureVisual) {
+            const _loci = structureVisual.getLoci(pickingId)
+            if (!isEmptyLoci(_loci)) loci = _loci
+        }
         return loci
     }
 
     function mark(loci: Loci, action: MarkerAction) {
         unitsVisuals.forEach(({ visual }) => visual.mark(loci, action))
+        if (structureVisual) structureVisual.mark(loci, action)
     }
 
     function destroy() {
         unitsVisuals.forEach(({ visual }) => visual.destroy())
         unitsVisuals.clear()
+        if (structureVisual) {
+            structureVisual.destroy()
+            structureVisual = undefined
+        }
     }
 
     return {
         get renderObjects() {
             const renderObjects: RenderObject[] = []
             unitsVisuals.forEach(({ visual }) => renderObjects.push(...visual.renderObjects))
+            if (structureVisual) renderObjects.push(...structureVisual.renderObjects)
             return renderObjects
         },
         create,

+ 2 - 1
src/mol-geo/representation/structure/visual/element-point.ts

@@ -14,7 +14,8 @@ import { fillSerial } from 'mol-gl/renderable/util';
 import { UnitsVisual, DefaultStructureProps } from '../index';
 import VertexMap from '../../../shape/vertex-map';
 import { SizeTheme } from '../../../theme';
-import { createTransforms, createColors, createSizes, markElement } from '../utils';
+import { markElement } from './util/element';
+import { createTransforms, createColors, createSizes } from './util/common';
 import { deepEqual, defaults } from 'mol-util';
 import { SortedArray, OrderedSet } from 'mol-data/int';
 import { RenderableState, PointValues } from 'mol-gl/renderable';

+ 2 - 1
src/mol-geo/representation/structure/visual/element-sphere.ts

@@ -11,7 +11,8 @@ import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/r
 import { Unit, Element } from 'mol-model/structure';
 import { DefaultStructureProps, UnitsVisual } from '../index';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors, createElementSphereMesh, markElement, getElementRadius } from '../utils';
+import { createTransforms, createColors } from '../visual/util/common';
+import { createElementSphereMesh, markElement, getElementRadius } from '../visual/util/element';
 import { deepEqual, defaults } from 'mol-util';
 import { fillSerial } from 'mol-gl/renderable/util';
 import { RenderableState, MeshValues } from 'mol-gl/renderable';

+ 204 - 0
src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts

@@ -0,0 +1,204 @@
+/**
+ * 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 { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Link, Structure } from 'mol-model/structure';
+import { DefaultStructureProps, StructureVisual } from '../index';
+import { RuntimeContext } from 'mol-task'
+import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link';
+import { fillSerial } from 'mol-gl/renderable/util';
+import { RenderableState, MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../../util/color-data';
+import { defaults } from 'mol-util';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
+import { SizeTheme } from '../../../theme';
+import { createIdentityTransform } from './util/common';
+// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+
+async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
+    const links = structure.links
+    const { bondCount, bonds } = links
+
+    if (!bondCount) return Mesh.createEmpty(mesh)
+
+    async function eachLink(ctx: RuntimeContext, cb: (edgeIndex: number, aI: number, bI: number) => void) {
+        for (let edgeIndex = 0, il = bondCount; edgeIndex < il; ++edgeIndex) {
+            const b = bonds[edgeIndex]
+            const aI = b.indexA, bI = b.indexB;
+            cb(edgeIndex, aI, bI)
+            if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'Cylinder mesh', current: edgeIndex, max: bondCount });
+            }
+        }
+    }
+
+    function getRefPos(aI: number, bI: number): Vec3 | null {
+        // TODO
+        return null
+    }
+
+    function setPositions(posA: Vec3, posB: Vec3, edgeIndex: number): void {
+        const b = bonds[edgeIndex]
+        const uA = b.unitA, uB = b.unitB
+        uA.conformation.position(uA.elements[b.indexA], posA)
+        uB.conformation.position(uB.elements[b.indexB], posB)
+    }
+
+    function getOrder(edgeIndex: number): number {
+        return bonds[edgeIndex].order
+    }
+
+    const linkBuilder = { linkCount: bondCount, eachLink, getRefPos, setPositions, getOrder }
+
+    return createLinkCylinderMesh(ctx, linkBuilder, props, mesh)
+}
+
+export const DefaultInterUnitLinkProps = {
+    ...DefaultStructureProps,
+    ...DefaultLinkCylinderProps,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+    flipSided: false,
+    flatShaded: false,
+}
+export type InterUnitLinkProps = Partial<typeof DefaultInterUnitLinkProps>
+
+export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
+    const renderObjects: RenderObject[] = []
+    let cylinders: MeshRenderObject
+    let currentProps: typeof DefaultInterUnitLinkProps
+    let mesh: Mesh
+    let currentStructure: Structure
+
+    return {
+        renderObjects,
+        async create(ctx: RuntimeContext, structure: Structure, props: InterUnitLinkProps = {}) {
+            currentProps = Object.assign({}, DefaultInterUnitLinkProps, props)
+
+            renderObjects.length = 0 // clear
+            currentStructure = structure
+
+            const elementCount = structure.links.bondCount
+            const instanceCount = 1
+
+            mesh = await createInterUnitLinkCylinderMesh(ctx, structure, currentProps)
+
+            if (ctx.shouldUpdate) await ctx.update('Computing link transforms');
+            const transforms = createIdentityTransform()
+
+            if (ctx.shouldUpdate) await ctx.update('Computing link colors');
+            const color = createUniformColor({ value: 0x999911 })
+            // const color = chainIdLinkColorData({ group, elementCount })
+
+            if (ctx.shouldUpdate) await ctx.update('Computing link marks');
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                aTransform: transforms,
+                aInstanceId: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
+                ...color,
+                ...marker,
+
+                uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
+                uInstanceCount: ValueCell.create(instanceCount),
+                uElementCount: ValueCell.create(elementCount),
+
+                elements: mesh.indexBuffer,
+
+                drawCount: ValueCell.create(mesh.triangleCount * 3),
+                instanceCount: ValueCell.create(instanceCount),
+
+                dDoubleSided: ValueCell.create(defaults(props.doubleSided, true)),
+                dFlatShaded: ValueCell.create(defaults(props.flatShaded, false)),
+                dFlipSided: ValueCell.create(defaults(props.flipSided, false)),
+                dUseFog: ValueCell.create(defaults(props.useFog, true)),
+            }
+            const state: RenderableState = {
+                depthMask: defaults(props.depthMask, true),
+                visible: defaults(props.visible, true)
+            }
+
+            cylinders = createMeshRenderObject(values, state)
+            renderObjects.push(cylinders)
+        },
+        async update(ctx: RuntimeContext, props: InterUnitLinkProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!cylinders) return false
+            // TODO
+
+            ValueCell.updateIfChanged(cylinders.values.uAlpha, newProps.alpha)
+            ValueCell.updateIfChanged(cylinders.values.dDoubleSided, newProps.doubleSided)
+            ValueCell.updateIfChanged(cylinders.values.dFlipSided, newProps.flipSided)
+            ValueCell.updateIfChanged(cylinders.values.dFlatShaded, newProps.flatShaded)
+
+            cylinders.state.visible = newProps.visible
+            cylinders.state.depthMask = newProps.depthMask
+
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getLinkLoci(pickingId, currentStructure, cylinders.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markLink(loci, action, currentStructure, cylinders.values)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, elementId } = pickingId
+    if (id === objectId) {
+        const bond = structure.links.bonds[elementId]
+        return Link.Loci([{
+            aUnit: bond.unitA,
+            aIndex: bond.indexA,
+            bUnit: bond.unitB,
+            bIndex: bond.indexB
+        }])
+    }
+    return EmptyLoci
+}
+
+function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) {
+    const tMarker = values.tMarker
+
+    const links = structure.links
+    const elementCount = links.bondCount
+    const instanceCount = 1
+
+    let changed = false
+    const array = tMarker.ref.value.array
+    if (isEveryLoci(loci)) {
+        applyMarkerAction(array, 0, elementCount * instanceCount, action)
+        changed = true
+    } else if (Link.isLoci(loci)) {
+        for (const b of loci.links) {
+            const _idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
+            if (_idx !== -1) {
+                const idx = _idx
+                if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
+                    changed = true
+                }
+            }
+        }
+    } else {
+        return
+    }
+    if (changed) {
+        ValueCell.update(tMarker, tMarker.ref.value)
+    }
+}

+ 25 - 133
src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts

@@ -5,151 +5,73 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-// TODO multiple cylinders for higher bond orders
-
 import { ValueCell } from 'mol-util/value-cell'
 
 import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
 import { Unit, Link } from 'mol-model/structure';
 import { UnitsVisual, DefaultStructureProps } from '../index';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms } from '../utils';
+import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh } from './util/link';
 import { fillSerial } from 'mol-gl/renderable/util';
 import { RenderableState, MeshValues } from 'mol-gl/renderable';
 import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
-import { MeshBuilder } from '../../../shape/mesh-builder';
-import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { Vec3 } from 'mol-math/linear-algebra';
 // import { createUniformColor } from '../../../util/color-data';
 import { defaults } from 'mol-util';
 import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
 import { SizeTheme } from '../../../theme';
 import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+import { createTransforms } from './util/common';
 
-const DefaultLinkCylinderProps = {
-    linkScale: 0.4,
-    linkSpacing: 1,
-    linkRadius: 0.25,
-    radialSegments: 16
-}
-type LinkCylinderProps = typeof DefaultLinkCylinderProps
-
-async function createLinkCylinderMesh(ctx: RuntimeContext, unit: Unit, props: LinkCylinderProps, mesh?: Mesh) {
+async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit, props: LinkCylinderProps, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
 
     const elements = unit.elements;
     const links = unit.links
     const { edgeCount, a, b, edgeProps, offset } = links
-    const orders = edgeProps.order
+    const { order } = edgeProps
 
     if (!edgeCount) return Mesh.createEmpty(mesh)
 
-    // approximate vertextCount, exact calculation would need to take link orders into account
-    const vertexCount = 32 * edgeCount
-    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
-
-    const va = Vec3.zero()
-    const vb = Vec3.zero()
-    const vd = Vec3.zero()
-    const vc = Vec3.zero()
-    const m = Mat4.identity()
-    const mt = Mat4.identity()
-
-    const vShift = Vec3.zero()
-    const vCenter = Vec3.zero()
     const vRef = Vec3.zero()
 
-    const { linkScale, linkSpacing, linkRadius, radialSegments } = props
+    const pos = unit.conformation.invariantPosition
 
-    const cylinderParams = {
-        height: 1,
-        radiusTop: linkRadius,
-        radiusBottom: linkRadius,
-        radialSegments,
-        openEnded: true
+    async function eachLink(ctx: RuntimeContext, cb: (edgeIndex: number, aI: number, bI: number) => void) {
+        for (let edgeIndex = 0, _eI = edgeCount * 2; edgeIndex < _eI; ++edgeIndex) {
+            const aI = a[edgeIndex], bI = b[edgeIndex];
+            cb(edgeIndex, aI, bI)
+            if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'Cylinder mesh', current: edgeIndex, max: edgeCount });
+            }
+        }
     }
 
-    const pos = unit.conformation.invariantPosition
-    // const l = Element.Location()
-    // l.unit = unit
-
-    // assumes aI < bI
-    function getRefPos(aI: number, bI: number) {
-        if (aI > bI) console.log('aI > bI')
+    function getRefPos(aI: number, bI: number): Vec3 | null {
         for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
             if (b[i] !== bI) return pos(elements[b[i]], vRef)
         }
         for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
             if (a[i] !== aI) return pos(elements[a[i]], vRef)
         }
-        // console.log('no ref', aI, bI, unit.model.atomicHierarchy.atoms.auth_atom_id.value(aI), unit.model.atomicHierarchy.atoms.auth_atom_id.value(bI), offset[aI], offset[aI + 1], offset[bI], offset[bI + 1], offset)
         return null
     }
 
-    for (let edgeIndex = 0, _eI = edgeCount * 2; edgeIndex < _eI; ++edgeIndex) {
-        const aI = elements[a[edgeIndex]], bI = elements[b[edgeIndex]];
-        // Each edge is included twice to allow for coloring/picking
-        // the half closer to the first vertex, i.e. vertex a.
-        pos(aI, va)
-        pos(bI, vb)
-        const d = Vec3.distance(va, vb)
-
-        Vec3.sub(vd, vb, va)
-        Vec3.scale(vd, Vec3.normalize(vd, vd), d / 4)
-        Vec3.add(vc, va, vd)
-        // ensure both edge halfs are pointing in the the same direction so the triangles align
-        if (aI > bI) Vec3.scale(vd, vd, -1)
-        Vec3.makeRotation(m, Vec3.create(0, 1, 0), vd)
-
-        const order = orders[edgeIndex]
-        meshBuilder.setId(edgeIndex)
-        cylinderParams.height = d / 2
-
-        if (order === 2 || order === 3) {
-            const multiRadius = linkRadius * (linkScale / (0.5 * order))
-            const absOffset = (linkRadius - multiRadius) * linkSpacing
-
-            if (aI < bI) {
-                calculateShiftDir(vShift, va, vb, getRefPos(a[edgeIndex], b[edgeIndex]))
-            } else {
-                calculateShiftDir(vShift, vb, va, getRefPos(b[edgeIndex], a[edgeIndex]))
-            }
-            Vec3.setMagnitude(vShift, vShift, absOffset)
-
-            cylinderParams.radiusTop = multiRadius
-            cylinderParams.radiusBottom = multiRadius
-
-            if (order === 3) {
-                Mat4.fromTranslation(mt, vc)
-                Mat4.mul(mt, mt, m)
-                meshBuilder.addCylinder(mt, cylinderParams)
-            }
-
-            Vec3.add(vCenter, vc, vShift)
-            Mat4.fromTranslation(mt, vCenter)
-            Mat4.mul(mt, mt, m)
-            meshBuilder.addCylinder(mt, cylinderParams)
-
-            Vec3.sub(vCenter, vc, vShift)
-            Mat4.fromTranslation(mt, vCenter)
-            Mat4.mul(mt, mt, m)
-            meshBuilder.addCylinder(mt, cylinderParams)
-        } else {
-            cylinderParams.radiusTop = linkRadius
-            cylinderParams.radiusBottom = linkRadius
-
-            Mat4.setTranslation(m, vc)
-            meshBuilder.addCylinder(m, cylinderParams)
-        }
+    function setPositions(posA: Vec3, posB: Vec3, edgeIndex: number): void {
+        pos(elements[a[edgeIndex]], posA)
+        pos(elements[b[edgeIndex]], posB)
+    }
 
-        if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) {
-            await ctx.update({ message: 'Cylinder mesh', current: edgeIndex, max: edgeCount });
-        }
+    function getOrder(edgeIndex: number): number {
+        return order[edgeIndex]
     }
 
-    return meshBuilder.getMesh()
+    const builder = { linkCount: edgeCount, eachLink, getRefPos, setPositions, getOrder }
+
+    return createLinkCylinderMesh(ctx, builder, props, mesh)
 }
 
 export const DefaultIntraUnitLinkProps = {
@@ -180,7 +102,7 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
             const elementCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
             const instanceCount = group.units.length
 
-            mesh = await createLinkCylinderMesh(ctx, unit, currentProps)
+            mesh = await createIntraUnitLinkCylinderMesh(ctx, unit, currentProps)
 
             if (ctx.shouldUpdate) await ctx.update('Computing link transforms');
             const transforms = createTransforms(group)
@@ -295,34 +217,4 @@ function markLink(loci: Loci, action: MarkerAction, group: Unit.SymmetryGroup, v
     if (changed) {
         ValueCell.update(tMarker, tMarker.ref.value)
     }
-}
-
-const tmpShiftV12 = Vec3.zero()
-const tmpShiftV13 = Vec3.zero()
-
-/** Calculate 'shift' direction that is perpendiculat to v1 - v2 and goes through v3 */
-function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | null) {
-    Vec3.sub(tmpShiftV12, v1, v2)
-
-    if (v3 !== null) {
-        Vec3.sub(tmpShiftV13, v1, v3)
-    } else {
-        Vec3.copy(tmpShiftV13, v1)  // no reference point, use v1
-    }
-    Vec3.normalize(tmpShiftV13, tmpShiftV13)
-
-    // ensure v13 and v12 are not colinear
-    let dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
-    if (1 - Math.abs(dp) < 1e-5) {
-        Vec3.set(tmpShiftV13, 1, 0, 0)
-        dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
-        if (1 - Math.abs(dp) < 1e-5) {
-            Vec3.set(tmpShiftV13, 0, 1, 0)
-            dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
-        }
-    }
-
-    Vec3.setMagnitude(tmpShiftV12, tmpShiftV12, dp)
-    Vec3.sub(tmpShiftV13, tmpShiftV13, tmpShiftV12)
-    return Vec3.normalize(out, tmpShiftV13)
 }

+ 71 - 0
src/mol-geo/representation/structure/visual/util/common.ts

@@ -0,0 +1,71 @@
+
+
+/**
+ * Copyright (c) 2018 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>
+ */
+
+import { Unit } from 'mol-model/structure';
+import { Mat4 } from 'mol-math/linear-algebra'
+
+import { createUniformColor, ColorData } from '../../../../util/color-data';
+import { createUniformSize, SizeData } from '../../../../util/size-data';
+import { physicalSizeData } from '../../../../theme/structure/size/physical';
+import VertexMap from '../../../../shape/vertex-map';
+import { ColorTheme, SizeTheme } from '../../../../theme';
+import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdElementColorData } from '../../../../theme/structure/color';
+import { ValueCell, defaults } from 'mol-util';
+
+export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
+    const unitCount = units.length
+    const n = unitCount * 16
+    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
+    for (let i = 0; i < unitCount; i++) {
+        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
+    }
+    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
+}
+
+const identityTransform = new Float32Array(16)
+Mat4.toArray(Mat4.identity(), identityTransform, 0)
+export function createIdentityTransform(transforms?: ValueCell<Float32Array>) {
+    return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform)
+}
+
+export function createColors(group: Unit.SymmetryGroup, elementCount: number, props: ColorTheme, colorData?: ColorData) {
+    switch (props.name) {
+        case 'atom-index':
+            return elementIndexColorData({ group, elementCount }, colorData)
+        case 'chain-id':
+            return chainIdElementColorData({ group, elementCount }, colorData)
+        case 'element-symbol':
+            return elementSymbolColorData({ group, elementCount }, colorData)
+        case 'instance-index':
+            return instanceIndexColorData({ group, elementCount }, colorData)
+        case 'uniform':
+            return createUniformColor(props, colorData)
+    }
+}
+
+// export function createLinkColors(group: Unit.SymmetryGroup, props: ColorTheme, colorData?: ColorData): ColorData {
+//     switch (props.name) {
+//         case 'atom-index':
+//         case 'chain-id':
+//         case 'element-symbol':
+//         case 'instance-index':
+//             return chainIdLinkColorData({ group, vertexMap }, colorData)
+//         case 'uniform':
+//             return createUniformColor(props, colorData)
+//     }
+// }
+
+export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: SizeTheme): SizeData {
+    switch (props.name) {
+        case 'uniform':
+            return createUniformSize(props)
+        case 'physical':
+            return physicalSizeData(defaults(props.factor, 1), { group, vertexMap })
+    }
+}

+ 9 - 61
src/mol-geo/representation/structure/utils.ts → src/mol-geo/representation/structure/visual/util/element.ts

@@ -2,73 +2,21 @@
  * Copyright (c) 2018 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>
  */
 
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { Unit, Element } from 'mol-model/structure';
-import { Mat4, Vec3 } from 'mol-math/linear-algebra'
-
-import { createUniformColor, ColorData } from '../../util/color-data';
-import { createUniformSize, SizeData } from '../../util/size-data';
-import { physicalSizeData, getPhysicalRadius } from '../../theme/structure/size/physical';
-import VertexMap from '../../shape/vertex-map';
-import { ColorTheme, SizeTheme } from '../../theme';
-import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdElementColorData } from '../../theme/structure/color';
-import { ValueCell, defaults } from 'mol-util';
-import { Mesh } from '../../shape/mesh';
+import { SizeTheme } from '../../../../theme';
 import { RuntimeContext } from 'mol-task';
-import { icosahedronVertexCount } from '../../primitive/icosahedron';
-import { MeshBuilder } from '../../shape/mesh-builder';
+import { icosahedronVertexCount } from '../../../../primitive/icosahedron';
+import { Mesh } from '../../../../shape/mesh';
+import { MeshBuilder } from '../../../../shape/mesh-builder';
+import { ValueCell, defaults } from 'mol-util';
 import { TextureImage } from 'mol-gl/renderable/util';
-import { applyMarkerAction, MarkerAction } from '../../util/marker-data';
 import { Loci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction } from '../../../../util/marker-data';
 import { Interval } from 'mol-data/int';
-
-export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
-    const unitCount = units.length
-    const n = unitCount * 16
-    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
-    for (let i = 0; i < unitCount; i++) {
-        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
-    }
-    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
-}
-
-export function createColors(group: Unit.SymmetryGroup, elementCount: number, props: ColorTheme, colorData?: ColorData) {
-    switch (props.name) {
-        case 'atom-index':
-            return elementIndexColorData({ group, elementCount }, colorData)
-        case 'chain-id':
-            return chainIdElementColorData({ group, elementCount }, colorData)
-        case 'element-symbol':
-            return elementSymbolColorData({ group, elementCount }, colorData)
-        case 'instance-index':
-            return instanceIndexColorData({ group, elementCount }, colorData)
-        case 'uniform':
-            return createUniformColor(props, colorData)
-    }
-}
-
-// export function createLinkColors(group: Unit.SymmetryGroup, props: ColorTheme, colorData?: ColorData): ColorData {
-//     switch (props.name) {
-//         case 'atom-index':
-//         case 'chain-id':
-//         case 'element-symbol':
-//         case 'instance-index':
-//             return chainIdLinkColorData({ group, vertexMap }, colorData)
-//         case 'uniform':
-//             return createUniformColor(props, colorData)
-//     }
-// }
-
-export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: SizeTheme): SizeData {
-    switch (props.name) {
-        case 'uniform':
-            return createUniformSize(props)
-        case 'physical':
-            return physicalSizeData(defaults(props.factor, 1), { group, vertexMap })
-    }
-}
+import { getPhysicalRadius } from '../../../../theme/structure/size/physical';
 
 export function getElementRadius(unit: Unit, props: SizeTheme): Element.Property<number> {
     switch (props.name) {
@@ -145,4 +93,4 @@ export function markElement(tMarker: ValueCell<TextureImage>, group: Unit.Symmet
     if (changed) {
         ValueCell.update(tMarker, tMarker.ref.value)
     }
-}
+}

+ 149 - 0
src/mol-geo/representation/structure/visual/util/link.ts

@@ -0,0 +1,149 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { RuntimeContext } from 'mol-task';
+import { Mesh } from '../../../../shape/mesh';
+import { MeshBuilder } from '../../../../shape/mesh-builder';
+
+export const DefaultLinkCylinderProps = {
+    linkScale: 0.4,
+    linkSpacing: 1,
+    linkRadius: 0.25,
+    radialSegments: 16
+}
+export type LinkCylinderProps = typeof DefaultLinkCylinderProps
+
+const tmpShiftV12 = Vec3.zero()
+const tmpShiftV13 = Vec3.zero()
+
+/** Calculate 'shift' direction that is perpendiculat to v1 - v2 and goes through v3 */
+export function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | null) {
+    Vec3.sub(tmpShiftV12, v1, v2)
+
+    if (v3 !== null) {
+        Vec3.sub(tmpShiftV13, v1, v3)
+    } else {
+        Vec3.copy(tmpShiftV13, v1)  // no reference point, use v1
+    }
+    Vec3.normalize(tmpShiftV13, tmpShiftV13)
+
+    // ensure v13 and v12 are not colinear
+    let dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+    if (1 - Math.abs(dp) < 1e-5) {
+        Vec3.set(tmpShiftV13, 1, 0, 0)
+        dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+        if (1 - Math.abs(dp) < 1e-5) {
+            Vec3.set(tmpShiftV13, 0, 1, 0)
+            dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+        }
+    }
+
+    Vec3.setMagnitude(tmpShiftV12, tmpShiftV12, dp)
+    Vec3.sub(tmpShiftV13, tmpShiftV13, tmpShiftV12)
+    return Vec3.normalize(out, tmpShiftV13)
+}
+
+export interface LinkCylinderMeshBuilder {
+    linkCount: number
+    eachLink(ctx: RuntimeContext, cb: (edgeIndex: number, aI: number, bI: number) => void): Promise<void>
+    // assumes aI < bI
+    getRefPos(aI: number, bI: number): Vec3 | null
+    setPositions(posA: Vec3, posB: Vec3, edgeIndex: number): void
+    getOrder(edgeIndex: number): number
+}
+
+/**
+ * Each edge is included twice to allow for coloring/picking
+ * the half closer to the first vertex, i.e. vertex a.
+ */
+export async function createLinkCylinderMesh(ctx: RuntimeContext, linkBuilder: LinkCylinderMeshBuilder, props: LinkCylinderProps, mesh?: Mesh) {
+    const { linkCount, eachLink, getRefPos, setPositions, getOrder } = linkBuilder
+
+    if (!linkCount) return Mesh.createEmpty(mesh)
+
+    // approximate vertextCount, exact calculation would need to take link orders into account
+    const vertexCount = 32 * linkCount
+    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
+
+    const va = Vec3.zero()
+    const vb = Vec3.zero()
+    const vd = Vec3.zero()
+    const vc = Vec3.zero()
+    const m = Mat4.identity()
+    const mt = Mat4.identity()
+
+    const vShift = Vec3.zero()
+    const vCenter = Vec3.zero()
+
+    const { linkScale, linkSpacing, linkRadius, radialSegments } = props
+
+    const cylinderParams = {
+        height: 1,
+        radiusTop: linkRadius,
+        radiusBottom: linkRadius,
+        radialSegments,
+        openEnded: true
+    }
+
+    // for (let edgeIndex = 0, _eI = edgeCount * 2; edgeIndex < _eI; ++edgeIndex) {
+    await eachLink(ctx, (edgeIndex, aI, bI) => {
+        // const aI = a[edgeIndex], bI = b[edgeIndex];
+
+        setPositions(va, vb, edgeIndex)
+        const d = Vec3.distance(va, vb)
+
+        Vec3.sub(vd, vb, va)
+        Vec3.scale(vd, Vec3.normalize(vd, vd), d / 4)
+        Vec3.add(vc, va, vd)
+        // ensure both edge halfs are pointing in the the same direction so the triangles align
+        if (aI > bI) Vec3.scale(vd, vd, -1)
+        Vec3.makeRotation(m, Vec3.create(0, 1, 0), vd)
+
+        const order = getOrder(edgeIndex)
+        meshBuilder.setId(edgeIndex)
+        cylinderParams.height = d / 2
+
+        if (order === 2 || order === 3) {
+            const multiRadius = linkRadius * (linkScale / (0.5 * order))
+            const absOffset = (linkRadius - multiRadius) * linkSpacing
+
+            if (aI < bI) {
+                calculateShiftDir(vShift, va, vb, getRefPos(aI, bI))
+            } else {
+                calculateShiftDir(vShift, vb, va, getRefPos(bI, aI))
+            }
+            Vec3.setMagnitude(vShift, vShift, absOffset)
+
+            cylinderParams.radiusTop = multiRadius
+            cylinderParams.radiusBottom = multiRadius
+
+            if (order === 3) {
+                Mat4.fromTranslation(mt, vc)
+                Mat4.mul(mt, mt, m)
+                meshBuilder.addCylinder(mt, cylinderParams)
+            }
+
+            Vec3.add(vCenter, vc, vShift)
+            Mat4.fromTranslation(mt, vCenter)
+            Mat4.mul(mt, mt, m)
+            meshBuilder.addCylinder(mt, cylinderParams)
+
+            Vec3.sub(vCenter, vc, vShift)
+            Mat4.fromTranslation(mt, vCenter)
+            Mat4.mul(mt, mt, m)
+            meshBuilder.addCylinder(mt, cylinderParams)
+        } else {
+            cylinderParams.radiusTop = linkRadius
+            cylinderParams.radiusBottom = linkRadius
+
+            Mat4.setTranslation(m, vc)
+            meshBuilder.addCylinder(m, cylinderParams)
+        }
+    })
+
+    return meshBuilder.getMesh()
+}

+ 48 - 0
src/mol-model/structure/structure/unit/links/data.ts

@@ -16,12 +16,47 @@ namespace IntraUnitLinks {
 }
 
 class InterUnitBonds {
+    readonly bondCount: number
+    readonly bonds: ReadonlyArray<InterUnitBonds.Bond>
+    private readonly bondKeyIndex: Map<string, number>
+
     getLinkedUnits(unit: Unit): ReadonlyArray<InterUnitBonds.UnitPairBonds> {
         if (!this.map.has(unit.id)) return emptyArray;
         return this.map.get(unit.id)!;
     }
 
+    /** Index into this.bonds */
+    getBondIndex(indexA: number, unitA: Unit, indexB: number, unitB: Unit): number {
+        const key = InterUnitBonds.getBondKey(indexA, unitA, indexB, unitB)
+        const index = this.bondKeyIndex.get(key)
+        return index !== undefined ? index : -1
+    }
+
+    getBond(indexA: number, unitA: Unit, indexB: number, unitB: Unit): InterUnitBonds.Bond | undefined {
+        const index = this.getBondIndex(indexA, unitA, indexB, unitB)
+        return index !== -1 ? this.bonds[index] : undefined
+    }
+
     constructor(private map: Map<number, InterUnitBonds.UnitPairBonds[]>) {
+        let count = 0
+        const bonds: (InterUnitBonds.Bond)[] = []
+        const bondKeyIndex = new Map<string, number>()
+        this.map.forEach(pairBondsArray => {
+            pairBondsArray.forEach(pairBonds => {
+                count += pairBonds.bondCount
+                pairBonds.linkedElementIndices.forEach(indexA => {
+                    pairBonds.getBonds(indexA).forEach(bondInfo => {
+                        const { unitA, unitB } = pairBonds
+                        const key = InterUnitBonds.getBondKey(indexA, unitA, bondInfo.indexB, unitB)
+                        bondKeyIndex.set(key, bonds.length)
+                        bonds.push({ ...bondInfo, indexA, unitA, unitB })
+                    })
+                })
+            })
+        })
+        this.bondCount = count
+        this.bonds = bonds
+        this.bondKeyIndex = bondKeyIndex
     }
 }
 
@@ -52,6 +87,19 @@ namespace InterUnitBonds {
         readonly order: number,
         readonly flag: LinkType.Flag
     }
+
+    export interface Bond {
+        readonly unitA: Unit.Atomic,
+        readonly unitB: Unit.Atomic,
+        readonly indexA: number,
+        readonly indexB: number,
+        readonly order: number,
+        readonly flag: LinkType.Flag
+    }
+
+    export function getBondKey(indexA: number, unitA: Unit, indexB: number, unitB: Unit) {
+        return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
+    }
 }
 
 const emptyArray: any[] = [];

+ 3 - 1
src/mol-view/stage.ts

@@ -41,8 +41,10 @@ export class Stage {
         this.ctx.viewer = this.viewer
 
         // this.loadPdbid('1jj2')
-        this.loadPdbid('4umt') // ligand has bond with order 3
+        // this.loadPdbid('4umt') // ligand has bond with order 3
         // this.loadPdbid('1crn')
+        this.loadPdbid('3pqr')
+        // this.loadPdbid('4v5a')
         // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
     }