From 66dc6fbb608804c7d1de3b2ea27b771f72cf649c Mon Sep 17 00:00:00 2001 From: Alexander Rose <alex.rose@rcsb.org> Date: Wed, 20 Jun 2018 18:56:14 -0700 Subject: [PATCH] wip, inter unit bonds --- src/apps/structure-info/model.ts | 70 +++--- .../structure/ball-and-stick.ts | 27 +-- src/mol-geo/representation/structure/index.ts | 12 +- .../structure/visual/element-point.ts | 3 +- .../structure/visual/element-sphere.ts | 3 +- .../visual/inter-unit-link-cylinder.ts | 204 ++++++++++++++++++ .../visual/intra-unit-link-cylinder.ts | 158 +++----------- .../structure/visual/util/common.ts | 71 ++++++ .../{utils.ts => visual/util/element.ts} | 70 +----- .../structure/visual/util/link.ts | 149 +++++++++++++ .../structure/structure/unit/links/data.ts | 48 +++++ src/mol-view/stage.ts | 4 +- 12 files changed, 581 insertions(+), 238 deletions(-) create mode 100644 src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts create mode 100644 src/mol-geo/representation/structure/visual/util/common.ts rename src/mol-geo/representation/structure/{utils.ts => visual/util/element.ts} (54%) create mode 100644 src/mol-geo/representation/structure/visual/util/link.ts diff --git a/src/apps/structure-info/model.ts b/src/apps/structure-info/model.ts index d63913dca..72557cb51 100644 --- a/src/apps/structure-info/model.ts +++ b/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) diff --git a/src/mol-geo/representation/structure/ball-and-stick.ts b/src/mol-geo/representation/structure/ball-and-stick.ts index 3e529fe8e..6038d6b56 100644 --- a/src/mol-geo/representation/structure/ball-and-stick.ts +++ b/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() } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/index.ts b/src/mol-geo/representation/structure/index.ts index ecb466cea..4101ec84c 100644 --- a/src/mol-geo/representation/structure/index.ts +++ b/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, diff --git a/src/mol-geo/representation/structure/visual/element-point.ts b/src/mol-geo/representation/structure/visual/element-point.ts index 2f487719c..c6bfaf84c 100644 --- a/src/mol-geo/representation/structure/visual/element-point.ts +++ b/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'; diff --git a/src/mol-geo/representation/structure/visual/element-sphere.ts b/src/mol-geo/representation/structure/visual/element-sphere.ts index d3b059276..f4ef2b605 100644 --- a/src/mol-geo/representation/structure/visual/element-sphere.ts +++ b/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'; diff --git a/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts new file mode 100644 index 000000000..569ae8c83 --- /dev/null +++ b/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) + } +} \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts index f0fea47c3..4449969d2 100644 --- a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts +++ b/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) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/util/common.ts b/src/mol-geo/representation/structure/visual/util/common.ts new file mode 100644 index 000000000..eee96d8bb --- /dev/null +++ b/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 }) + } +} diff --git a/src/mol-geo/representation/structure/utils.ts b/src/mol-geo/representation/structure/visual/util/element.ts similarity index 54% rename from src/mol-geo/representation/structure/utils.ts rename to src/mol-geo/representation/structure/visual/util/element.ts index 17aae12e0..c34a3d3d2 100644 --- a/src/mol-geo/representation/structure/utils.ts +++ b/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) } -} \ No newline at end of file +} diff --git a/src/mol-geo/representation/structure/visual/util/link.ts b/src/mol-geo/representation/structure/visual/util/link.ts new file mode 100644 index 000000000..a38d99837 --- /dev/null +++ b/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() +} \ No newline at end of file diff --git a/src/mol-model/structure/structure/unit/links/data.ts b/src/mol-model/structure/structure/unit/links/data.ts index dfd9762c5..ed046a026 100644 --- a/src/mol-model/structure/structure/unit/links/data.ts +++ b/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[] = []; diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 5133ae623..25d2ae191 100644 --- a/src/mol-view/stage.ts +++ b/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`) } -- GitLab