diff --git a/src/mol-app/ui/entity/tree.tsx b/src/mol-app/ui/entity/tree.tsx index cc889f7ff027455d15a18cf6e455d8d1c7d920b0..66eba7892b86c31e9a8bad1abc744a692d3a81f8 100644 --- a/src/mol-app/ui/entity/tree.tsx +++ b/src/mol-app/ui/entity/tree.tsx @@ -13,7 +13,7 @@ import { View } from '../view'; import { EntityTreeController } from '../../controller/entity/tree'; import { Controller } from '../../controller/controller'; import { AnyEntity, RootEntity } from 'mol-view/state/entity'; -import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill, StructureCenter } from 'mol-view/state/transform'; +import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill, StructureCenter, StructureToBond, BondUpdate } from 'mol-view/state/transform'; function getTransforms(entity: AnyEntity): AnyTransform[] { const transforms: AnyTransform[] = [] @@ -40,11 +40,14 @@ function getTransforms(entity: AnyEntity): AnyTransform[] { transforms.push(ModelToStructure) break; case 'structure': - transforms.push(StructureToSpacefill, StructureCenter) + transforms.push(StructureToSpacefill, StructureToBond, StructureCenter) break; case 'spacefill': transforms.push(SpacefillUpdate) break; + case 'bond': + transforms.push(BondUpdate) + break; } return transforms } diff --git a/src/mol-app/ui/transform/bond.tsx b/src/mol-app/ui/transform/bond.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cc87a6dc8dae7bde67ab45f6b1880b823e215b94 --- /dev/null +++ b/src/mol-app/ui/transform/bond.tsx @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol + * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' + +import { View } from '../view'; +import { Controller } from '../../controller/controller'; +import { Toggle } from '../controls/common'; +import { BondEntity } from 'mol-view/state/entity'; +import { BondUpdate } from 'mol-view/state/transform' +import { StateContext } from 'mol-view/state/context'; +import { ColorTheme } from 'mol-geo/theme'; +import { Color, ColorNames } from 'mol-util/color'; +import { Slider } from '../controls/slider'; + +export const ColorThemeInfo = { + 'atom-index': {}, + 'chain-id': {}, + 'element-symbol': {}, + 'instance-index': {}, + 'uniform': {} +} +export type ColorThemeInfo = keyof typeof ColorThemeInfo + +interface BondState { + doubleSided: boolean + flipSided: boolean + flatShaded: boolean + colorTheme: ColorTheme + colorValue: Color + visible: boolean + alpha: number + depthMask: boolean +} + +export class Bond extends View<Controller<any>, BondState, { transform: BondUpdate, entity: BondEntity, ctx: StateContext }> { + state = { + doubleSided: true, + flipSided: false, + flatShaded: false, + colorTheme: { name: 'element-symbol' } as ColorTheme, + colorValue: 0x000000, + visible: true, + alpha: 1, + depthMask: true + } + + update(state?: Partial<BondState>) { + const { transform, entity, ctx } = this.props + const newState = { ...this.state, ...state } + this.setState(newState) + transform.apply(ctx, entity, newState) + } + + render() { + const { transform } = this.props + + const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => { + return <option key={name} value={name}>{name}</option> + }) + + const colorValueOptions = Object.keys(ColorNames).map((name, idx) => { + return <option key={name} value={(ColorNames as any)[name]}>{name}</option> + }) + + return <div className='molstar-transformer-wrapper'> + <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'> + <div className='molstar-panel-header'> + <button + className='molstar-btn molstar-btn-link molstar-panel-expander' + onClick={() => this.update()} + > + <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span> + </button> + </div> + <div className='molstar-panel-body'> + <div> + <div className='molstar-control-row molstar-options-group'> + <span>Color theme</span> + <div> + <select + className='molstar-form-control' + value={this.state.colorTheme.name} + onChange={(e) => { + const colorThemeName = e.target.value as ColorThemeInfo + if (colorThemeName === 'uniform') { + this.update({ + colorTheme: { + name: colorThemeName, + value: this.state.colorValue + } + }) + } else { + this.update({ + colorTheme: { name: colorThemeName } + }) + } + }} + > + {colorThemeOptions} + </select> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <span>Color value</span> + <div> + <select + className='molstar-form-control' + value={this.state.colorValue} + onChange={(e) => { + const colorValue = parseInt(e.target.value) + this.update({ + colorTheme: { + name: 'uniform', + value: colorValue + }, + colorValue + }) + }} + > + {colorValueOptions} + </select> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Toggle + value={this.state.visible} + label='Visibility' + onChange={value => this.update({ visible: value })} + /> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Toggle + value={this.state.depthMask} + label='Depth write' + onChange={value => this.update({ depthMask: value })} + /> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Toggle + value={this.state.doubleSided} + label='Double sided' + onChange={value => this.update({ doubleSided: value })} + /> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Toggle + value={this.state.flipSided} + label='Flip sided' + onChange={value => this.update({ flipSided: value })} + /> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Toggle + value={this.state.flatShaded} + label='Flat shaded' + onChange={value => this.update({ flatShaded: value })} + /> + </div> + </div> + <div className='molstar-control-row molstar-options-group'> + <div> + <Slider + value={this.state.alpha} + label='Opacity' + min={0} + max={1} + step={0.01} + callOnChangeWhileSliding={true} + onChange={value => this.update({ alpha: value })} + /> + </div> + </div> + </div> + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-app/ui/transform/list.tsx b/src/mol-app/ui/transform/list.tsx index dd3840ac610c2642ddf46a729794c886269df04d..8fa592ee0b86e64631679e277c652fd8c37a21fd 100644 --- a/src/mol-app/ui/transform/list.tsx +++ b/src/mol-app/ui/transform/list.tsx @@ -14,6 +14,7 @@ import { Controller } from '../../controller/controller'; import { TransformListController } from '../../controller/transform/list'; import { AnyTransform } from 'mol-view/state/transform'; import { Spacefill } from './spacefill'; +import { Bond } from './bond'; import { AnyEntity } from 'mol-view/state/entity'; import { FileLoader } from './file-loader'; import { ModelToStructure } from './model'; @@ -29,6 +30,8 @@ function getTransformComponent(controller: TransformListController, entity: AnyE return <StructureCenter controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></StructureCenter> case 'spacefill-update': return <Spacefill controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Spacefill> + case 'bond-update': + return <Bond controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Bond> } return <Transform controller={controller} entity={entity} transform={transform}></Transform> } diff --git a/src/mol-geo/representation/index.ts b/src/mol-geo/representation/index.ts index 3bb9db0d29abeaa5fca0a07c80490c0fd0da4b6f..7c2d2de3657ed3bdbbf102bf164848419dbd1a38 100644 --- a/src/mol-geo/representation/index.ts +++ b/src/mol-geo/representation/index.ts @@ -5,8 +5,10 @@ */ import { Task } from 'mol-task' -import { RenderObject } from 'mol-gl/render-object'; -import { PickingId, PickingInfo } from '../util/picking'; +import { RenderObject } from 'mol-gl/render-object' +import { PickingId } from '../util/picking'; +import { Loci } from 'mol-model/loci'; +import { FlagAction } from '../util/flag-data'; export interface RepresentationProps {} @@ -14,5 +16,6 @@ export interface Representation<D, P extends RepresentationProps = {}> { renderObjects: ReadonlyArray<RenderObject> create: (data: D, props?: P) => Task<void> update: (props: P) => Task<void> - getLabel: (pickingId: PickingId) => PickingInfo | null + getLoci: (pickingId: PickingId) => Loci | null + applyFlags: (loci: Loci, action: FlagAction) => void } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/bond.ts b/src/mol-geo/representation/structure/bond.ts new file mode 100644 index 0000000000000000000000000000000000000000..afb2cf89c183e25db5c24fa9f47efa2d7784f8ad --- /dev/null +++ b/src/mol-geo/representation/structure/bond.ts @@ -0,0 +1,211 @@ +/** + * 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 { ValueCell } from 'mol-util/value-cell' + +import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object' +import { Unit, Element, Bond } from 'mol-model/structure'; +import { UnitsRepresentation, DefaultStructureProps } from './index'; +import { Task } from 'mol-task' +import { createTransforms } from './utils'; +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 { createUniformColor } from '../../util/color-data'; +import { defaults } from 'mol-util'; +import { Loci, isEveryLoci } from 'mol-model/loci'; +import { FlagAction, applyFlagAction, createFlags } from '../../util/flag-data'; + +function createBondMesh(unit: Unit, mesh?: Mesh) { + return Task.create('Cylinder mesh', async ctx => { + if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh) + + const elements = unit.elements; + const bonds = unit.bonds + const { edgeCount, a, b } = bonds + + if (!edgeCount) return Mesh.createEmpty(mesh) + + // TODO calculate vertextCount properly + const vertexCount = 32 * edgeCount + const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh) + + const va = Vec3.zero() + const vb = Vec3.zero() + const vt = Vec3.zero() + const m = Mat4.identity() + + const { x, y, z } = unit.conformation + const l = Element.Location() + l.unit = unit + + for (let edgeIndex = 0, _eI = edgeCount * 2; edgeIndex < _eI; ++edgeIndex) { + const aI = elements[a[edgeIndex]], bI = elements[b[edgeIndex]]; + // each edge is included twice because of the "adjacency list" structure + // keep only the 1st occurence. + if (aI >= bI) continue; + va[0] = x(aI); va[1] = y(aI); va[2] = z(aI) + vb[0] = x(bI); vb[1] = y(bI); vb[2] = z(bI) + + Vec3.scale(vt, Vec3.add(vt, va, vb), 0.5) + Vec3.makeRotation(m, Vec3.create(0, 1, 0), Vec3.sub(vb, vb, va)) + Mat4.setTranslation(m, vt) + + meshBuilder.setId(edgeIndex) + meshBuilder.addCylinder(m, { radiusTop: 0.2, radiusBottom: 0.2 }) + + if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Cylinder mesh', current: edgeIndex, max: edgeCount }); + } + } + + return meshBuilder.getMesh() + }) +} + +export const DefaultBondProps = { + ...DefaultStructureProps, + flipSided: false, + flatShaded: false, +} +export type BondProps = Partial<typeof DefaultBondProps> + +export default function BondUnitsRepresentation(): UnitsRepresentation<BondProps> { + const renderObjects: RenderObject[] = [] + let cylinders: MeshRenderObject + let currentProps: typeof DefaultBondProps + let mesh: Mesh + let currentGroup: Unit.SymmetryGroup + // let vertexMap: VertexMap + + return { + renderObjects, + create(group: Unit.SymmetryGroup, props: BondProps = {}) { + currentProps = Object.assign({}, DefaultBondProps, props) + + return Task.create('Bond.create', async ctx => { + renderObjects.length = 0 // clear + currentGroup = group + + const unit = group.units[0] + const elementCount = Unit.isAtomic(unit) ? unit.bonds.edgeCount * 2 : 0 + const instanceCount = group.units.length + + mesh = await createBondMesh(unit).runAsChild(ctx, 'Computing bond mesh') + + // console.log(mesh) + // vertexMap = VertexMap.fromMesh(mesh) + + await ctx.update('Computing bond transforms'); + const transforms = createTransforms(group) + + await ctx.update('Computing bond colors'); + const color = createUniformColor({ value: 0xFF0000 }) + + await ctx.update('Computing bond flags'); + const flag = createFlags(instanceCount * elementCount) + + const values: MeshValues = { + ...getMeshData(mesh), + aTransform: transforms, + aInstanceId: ValueCell.create(fillSerial(new Float32Array(instanceCount))), + ...color, + ...flag, + + 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)), + } + const state: RenderableState = { + depthMask: defaults(props.depthMask, true), + visible: defaults(props.visible, true) + } + + cylinders = createMeshRenderObject(values, state) + renderObjects.push(cylinders) + }) + }, + update(props: BondProps) { + const newProps = Object.assign({}, currentProps, props) + + return Task.create('Bond.update', async ctx => { + 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) { + const { objectId, instanceId, elementId } = pickingId + const unit = currentGroup.units[instanceId] + if (cylinders.id === objectId && Unit.isAtomic(unit)) { + return Bond.Loci([{ + aUnit: unit, + aIndex: unit.bonds.a[elementId], + bUnit: unit, + bIndex: unit.bonds.b[elementId] + }]) + } + return null + }, + applyFlags(loci: Loci, action: FlagAction) { + const group = currentGroup + const tFlag = cylinders.values.tFlag + const unit = group.units[0] + if (!Unit.isAtomic(unit)) return + + const elementCount = unit.bonds.edgeCount * 2 + const instanceCount = group.units.length + + let changed = false + const array = tFlag.ref.value.array + if (isEveryLoci(loci)) { + applyFlagAction(array, 0, elementCount * instanceCount, action) + changed = true + } else if (Bond.isLoci(loci)) { + for (const b of loci.bonds) { + const unitIdx = Unit.findUnitById(b.aUnit.id, group.units) + if (unitIdx !== -1) { + const _idx = unit.bonds.getEdgeIndex(b.aIndex, b.bIndex) + if (_idx !== -1) { + const idx = _idx + if (applyFlagAction(array, idx, idx + 1, action) && !changed) { + changed = true + } + } + } + } + } else { + return + } + if (changed) { + ValueCell.update(tFlag, tFlag.ref.value) + } + } + } +} diff --git a/src/mol-geo/representation/structure/index.ts b/src/mol-geo/representation/structure/index.ts index fdddec8de9566704904b87b7a04f1a4b60b0ce48..32dfa0a4bbadb1bf11e5f9a29a176adf9b614a73 100644 --- a/src/mol-geo/representation/structure/index.ts +++ b/src/mol-geo/representation/structure/index.ts @@ -5,70 +5,36 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Structure, StructureSymmetry, Unit, Element, Queries } from 'mol-model/structure'; +import { Structure, StructureSymmetry, Unit } from 'mol-model/structure'; import { Task } from 'mol-task' import { RenderObject } from 'mol-gl/render-object'; import { Representation, RepresentationProps } from '..'; import { ColorTheme } from '../../theme'; -import { PickingId, PickingInfo } from '../../util/picking'; +import { PickingId } from '../../util/picking'; +import { Loci } from 'mol-model/loci'; +import { FlagAction } from '../../util/flag-data'; export interface UnitsRepresentation<P> { renderObjects: ReadonlyArray<RenderObject> create: (group: Unit.SymmetryGroup, props: P) => Task<void> update: (props: P) => Task<boolean> - getLocation: (pickingId: PickingId) => Element.Location | null + getLoci: (pickingId: PickingId) => Loci | null + applyFlags: (loci: Loci, action: FlagAction) => void } -export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { - renderObjects: ReadonlyArray<RenderObject> - create: (structure: Structure, props?: P) => Task<void> - update: (props: P) => Task<void> - getLocation: (pickingId: PickingId) => Element.Location | null - getLabel: (pickingId: PickingId) => PickingInfo | null -} +export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { } interface GroupRepresentation<T> { repr: UnitsRepresentation<T> group: Unit.SymmetryGroup } -function label(loc: Element.Location) { - const model = loc.unit.model.label - const instance = loc.unit.conformation.operator.name - let element = '' - - if (Unit.isAtomic(loc.unit)) { - const asym_id = Queries.props.chain.auth_asym_id(loc) - const seq_id = Queries.props.residue.auth_seq_id(loc) - const comp_id = Queries.props.residue.auth_comp_id(loc) - const atom_id = Queries.props.atom.auth_atom_id(loc) - element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}` - } else if (Unit.isCoarse(loc.unit)) { - const asym_id = Queries.props.coarse.asym_id(loc) - const seq_id_begin = Queries.props.coarse.seq_id_begin(loc) - const seq_id_end = Queries.props.coarse.seq_id_end(loc) - if (seq_id_begin === seq_id_end) { - const entityKey = Queries.props.coarse.entityKey(loc) - const seq = loc.unit.model.sequence.byEntityKey[entityKey] - const comp_id = seq.compId.value(seq_id_begin) - element = `[${comp_id}]${seq_id_begin}:${asym_id}` - } else { - element = `${seq_id_begin}-${seq_id_end}:${asym_id}` - } - } else { - element = 'unknown' - } - - return { label: `${model} ${instance} ${element}` } -} - export const DefaultStructureProps = { colorTheme: { name: 'instance-index' } as ColorTheme, alpha: 1, visible: true, doubleSided: false, - depthMask: true, - hoverSelection: { objectId: -1, instanceId: -1, elementId: -1 } as PickingId + depthMask: true } export type StructureProps = Partial<typeof DefaultStructureProps> @@ -77,9 +43,9 @@ export function StructureRepresentation<P extends StructureProps>(reprCtor: () = const groupReprs: GroupRepresentation<P>[] = [] // let currentProps: typeof DefaultStructureProps - function getLocation(pickingId: PickingId) { + function getLoci(pickingId: PickingId) { for (let i = 0, il = groupReprs.length; i < il; ++i) { - const loc = groupReprs[i].repr.getLocation(pickingId) + const loc = groupReprs[i].repr.getLoci(pickingId) if (loc) return loc } return null @@ -121,10 +87,11 @@ export function StructureRepresentation<P extends StructureProps>(reprCtor: () = } }) }, - getLocation, - getLabel(pickingId: PickingId) { - const loc = getLocation(pickingId) - return loc ? label(loc) : null + getLoci, + applyFlags(loci: Loci, action: FlagAction) { + for (let i = 0, il = groupReprs.length; i < il; ++i) { + groupReprs[i].repr.applyFlags(loci, action) + } } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/point.ts b/src/mol-geo/representation/structure/point.ts index cf090acac8cafb697f1d0c96d60fe4794a843f5e..2daed89a676dc1330a36a4283b3b76d926222c42 100644 --- a/src/mol-geo/representation/structure/point.ts +++ b/src/mol-geo/representation/structure/point.ts @@ -14,11 +14,13 @@ import { fillSerial } from 'mol-gl/renderable/util'; import { UnitsRepresentation, DefaultStructureProps } from './index'; import VertexMap from '../../shape/vertex-map'; import { SizeTheme } from '../../theme'; -import { createTransforms, createColors, createSizes, createFlags } from './utils'; +import { createTransforms, createColors, createSizes, applyElementFlags } from './utils'; import { deepEqual, defaults } from 'mol-util'; import { SortedArray } from 'mol-data/int'; import { RenderableState, PointValues } from 'mol-gl/renderable'; import { PickingId } from '../../util/picking'; +import { Loci } from 'mol-model/loci'; +import { FlagAction, createFlags } from '../../util/flag-data'; export const DefaultPointProps = { ...DefaultStructureProps, @@ -45,7 +47,7 @@ export function createPointVertices(unit: Unit) { return vertices } -export default function Point(): UnitsRepresentation<PointProps> { +export default function PointUnitsRepresentation(): UnitsRepresentation<PointProps> { const renderObjects: RenderObject[] = [] let points: PointRenderObject let currentProps = DefaultPointProps @@ -66,8 +68,9 @@ export default function Point(): UnitsRepresentation<PointProps> { _units = group.units _elements = group.elements; - const { colorTheme, sizeTheme, hoverSelection } = currentProps + const { colorTheme, sizeTheme } = currentProps const elementCount = _elements.length + const instanceCount = group.units.length const vertexMap = VertexMap.create( elementCount, @@ -89,9 +92,7 @@ export default function Point(): UnitsRepresentation<PointProps> { const size = createSizes(group, vertexMap, sizeTheme) await ctx.update('Computing spacefill flags'); - const flag = createFlags(group, hoverSelection.instanceId, hoverSelection.elementId) - - const instanceCount = group.units.length + const flag = createFlags(instanceCount * elementCount) const values: PointValues = { aPosition: ValueCell.create(vertices), @@ -155,15 +156,17 @@ export default function Point(): UnitsRepresentation<PointProps> { return false }) }, - getLocation(pickingId: PickingId) { + getLoci(pickingId: PickingId) { const { objectId, instanceId, elementId } = pickingId if (points.id === objectId) { - const l = Element.Location() - l.unit = currentGroup.units[instanceId] - l.element = currentGroup.elements[elementId] - return l + const unit = currentGroup.units[instanceId] + const indices = SortedArray.ofSingleton(elementId) + return Element.Loci([{ unit, indices }]) } return null + }, + applyFlags(loci: Loci, action: FlagAction) { + applyElementFlags(points.values.tFlag, currentGroup, loci, action) } } } diff --git a/src/mol-geo/representation/structure/spacefill.ts b/src/mol-geo/representation/structure/spacefill.ts index ebfbe71a1c7c2923fa8d21fd2903bce1e4ff90ff..573c18547675ae1923077ddba3ede7d93e504a9f 100644 --- a/src/mol-geo/representation/structure/spacefill.ts +++ b/src/mol-geo/representation/structure/spacefill.ts @@ -11,7 +11,7 @@ import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/r import { Unit, Element, Queries } from 'mol-model/structure'; import { UnitsRepresentation, DefaultStructureProps } from './index'; import { Task } from 'mol-task' -import { createTransforms, createColors, createFlags, createEmptyFlags, createSphereMesh } from './utils'; +import { createTransforms, createColors, createSphereMesh, applyElementFlags } from './utils'; import VertexMap from '../../shape/vertex-map'; import { deepEqual, defaults } from 'mol-util'; import { fillSerial } from 'mol-gl/renderable/util'; @@ -19,6 +19,9 @@ import { RenderableState, MeshValues } from 'mol-gl/renderable'; import { getMeshData } from '../../util/mesh-data'; import { Mesh } from '../../shape/mesh'; import { PickingId } from '../../util/picking'; +import { SortedArray } from 'mol-data/int'; +import { createFlags, FlagAction } from '../../util/flag-data'; +import { Loci } from 'mol-model/loci'; function createSpacefillMesh(unit: Unit, detail: number, mesh?: Mesh) { let radius: Element.Property<number> @@ -30,7 +33,7 @@ function createSpacefillMesh(unit: Unit, detail: number, mesh?: Mesh) { console.warn('Unsupported unit type') return Task.constant('Empty mesh', Mesh.createEmpty(mesh)) } - return createSphereMesh(unit, radius, detail, mesh) + return createSphereMesh(unit, (l) => radius(l) * 0.3, detail, mesh) } export const DefaultSpacefillProps = { @@ -41,7 +44,7 @@ export const DefaultSpacefillProps = { } export type SpacefillProps = Partial<typeof DefaultSpacefillProps> -export default function Spacefill(): UnitsRepresentation<SpacefillProps> { +export default function SpacefillUnitsRepresentation(): UnitsRepresentation<SpacefillProps> { const renderObjects: RenderObject[] = [] let spheres: MeshRenderObject let currentProps: typeof DefaultSpacefillProps @@ -58,7 +61,9 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> { renderObjects.length = 0 // clear currentGroup = group - const { detail, colorTheme, hoverSelection } = { ...DefaultSpacefillProps, ...props } + const { detail, colorTheme } = { ...DefaultSpacefillProps, ...props } + const instanceCount = group.units.length + const elementCount = group.elements.length mesh = await createSpacefillMesh(group.units[0], detail).runAsChild(ctx, 'Computing spacefill mesh') // console.log(mesh) @@ -71,9 +76,7 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> { const color = createColors(group, vertexMap, colorTheme) await ctx.update('Computing spacefill flags'); - const flag = createFlags(group, hoverSelection.instanceId, hoverSelection.elementId) - - const instanceCount = group.units.length + const flag = createFlags(instanceCount * elementCount) const values: MeshValues = { ...getMeshData(mesh), @@ -84,7 +87,7 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> { uAlpha: ValueCell.create(defaults(props.alpha, 1.0)), uInstanceCount: ValueCell.create(instanceCount), - uElementCount: ValueCell.create(group.elements.length), + uElementCount: ValueCell.create(elementCount), elements: mesh.indexBuffer, @@ -129,15 +132,6 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> { createColors(currentGroup, vertexMap, newProps.colorTheme, spheres.values) } - if (newProps.hoverSelection !== currentProps.hoverSelection) { - await ctx.update('Computing spacefill flags'); - if (newProps.hoverSelection.objectId === spheres.id) { - createFlags(currentGroup, newProps.hoverSelection.instanceId, newProps.hoverSelection.elementId, spheres.values) - } else { - createEmptyFlags(spheres.values) - } - } - ValueCell.updateIfChanged(spheres.values.uAlpha, newProps.alpha) ValueCell.updateIfChanged(spheres.values.dDoubleSided, newProps.doubleSided) ValueCell.updateIfChanged(spheres.values.dFlipSided, newProps.flipSided) @@ -150,15 +144,17 @@ export default function Spacefill(): UnitsRepresentation<SpacefillProps> { return true }) }, - getLocation(pickingId: PickingId) { + getLoci(pickingId: PickingId) { const { objectId, instanceId, elementId } = pickingId if (spheres.id === objectId) { - const l = Element.Location() - l.unit = currentGroup.units[instanceId] - l.element = currentGroup.elements[elementId] - return l + const unit = currentGroup.units[instanceId] + const indices = SortedArray.ofSingleton(elementId); + return Element.Loci([{ unit, indices }]) } return null + }, + applyFlags(loci: Loci, action: FlagAction) { + applyElementFlags(spheres.values.tFlag, currentGroup, loci, action) } } } diff --git a/src/mol-geo/representation/structure/utils.ts b/src/mol-geo/representation/structure/utils.ts index 5771c3f122504e4413f1423ce0e73a8d53a92f6a..e6719eaa741ce5030a06dc361d81c1ffebbdbcdf 100644 --- a/src/mol-geo/representation/structure/utils.ts +++ b/src/mol-geo/representation/structure/utils.ts @@ -6,7 +6,7 @@ */ import { Unit, Element } from 'mol-model/structure'; -import { Mat4, Vec2, Vec3 } from 'mol-math/linear-algebra' +import { Mat4, Vec3 } from 'mol-math/linear-algebra' import { createUniformColor, ColorData } from '../../util/color-data'; import { createUniformSize } from '../../util/size-data'; @@ -15,11 +15,13 @@ import VertexMap from '../../shape/vertex-map'; import { ColorTheme, SizeTheme } from '../../theme'; import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdColorData } from '../../theme/structure/color'; import { ValueCell } from 'mol-util'; -import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; import { Mesh } from '../../shape/mesh'; import { Task } from 'mol-task'; import { icosahedronVertexCount } from '../../primitive/icosahedron'; import { MeshBuilder } from '../../shape/mesh-builder'; +import { TextureImage } from 'mol-gl/renderable/util'; +import { applyFlagAction, FlagAction } from '../../util/flag-data'; +import { Loci, isEveryLoci } from 'mol-model/loci'; export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) { const unitCount = units.length @@ -55,50 +57,6 @@ export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, pro } } -export type FlagData = { - tFlag: ValueCell<TextureImage> - uFlagTexSize: ValueCell<Vec2> -} - -export function createFlags(group: Unit.SymmetryGroup, instanceId: number, elementId: number, flagData?: FlagData): FlagData { - const instanceCount = group.units.length - const elementCount = group.elements.length - const count = instanceCount * elementCount - const flags = flagData && flagData.tFlag.ref.value.array.length >= count ? flagData.tFlag.ref.value : createTextureImage(count, 1) - let flagOffset = 0 - for (let i = 0; i < instanceCount; i++) { - for (let j = 0, jl = elementCount; j < jl; ++j) { - flags.array[flagOffset] = (i === instanceId && j === elementId) ? 255 : 0 - flagOffset += 1 - } - } - // console.log(flags, instanceCount, elementCount) - if (flagData) { - ValueCell.update(flagData.tFlag, flags) - ValueCell.update(flagData.uFlagTexSize, Vec2.create(flags.width, flags.height)) - return flagData - } else { - return { - tFlag: ValueCell.create(flags), - uFlagTexSize: ValueCell.create(Vec2.create(flags.width, flags.height)), - } - } -} - -const emptyFlagTexture = { array: new Uint8Array(1), width: 1, height: 1 } -export function createEmptyFlags(flagData?: FlagData) { - if (flagData) { - ValueCell.update(flagData.tFlag, emptyFlagTexture) - ValueCell.update(flagData.uFlagTexSize, Vec2.create(1, 1)) - return flagData - } else { - return { - tFlag: ValueCell.create(emptyFlagTexture), - uFlagTexSize: ValueCell.create(Vec2.create(1, 1)), - } - } -} - export function createSphereMesh(unit: Unit, radius: Element.Property<number>, detail: number, mesh?: Mesh) { return Task.create('Sphere mesh', async ctx => { const { elements } = unit; @@ -115,9 +73,7 @@ export function createSphereMesh(unit: Unit, radius: Element.Property<number>, d for (let i = 0; i < elementCount; i++) { l.element = elements[i] - v[0] = x(l.element) - v[1] = y(l.element) - v[2] = z(l.element) + v[0] = x(l.element); v[1] = y(l.element); v[2] = z(l.element) Mat4.setTranslation(m, v) meshBuilder.setId(i) @@ -131,3 +87,32 @@ export function createSphereMesh(unit: Unit, radius: Element.Property<number>, d return meshBuilder.getMesh() }) } + + +export function applyElementFlags(tFlag: ValueCell<TextureImage>, group: Unit.SymmetryGroup, loci: Loci, action: FlagAction) { + let changed = false + const elementCount = group.elements.length + const instanceCount = group.units.length + const array = tFlag.ref.value.array + if (isEveryLoci(loci)) { + applyFlagAction(array, 0, elementCount * instanceCount, action) + changed = true + } else if (Element.isLoci(loci)) { + for (const e of loci.elements) { + const unitIdx = Unit.findUnitById(e.unit.id, group.units) + if (unitIdx !== -1) { + for (let i = 0, il = e.indices.length; i < il; ++i) { + const idx = unitIdx * elementCount + e.indices[i] + if (applyFlagAction(array, idx, idx + 1, action) && !changed) { + changed = true + } + } + } + } + } else { + return + } + if (changed) { + ValueCell.update(tFlag, tFlag.ref.value) + } +} \ No newline at end of file diff --git a/src/mol-geo/representation/volume/index.ts b/src/mol-geo/representation/volume/index.ts index f8edfdaad75d5dbce7d9e2c4fad7a793b9d02d56..cc702bfbd9f84a35830a1332ff173cfa8e375a2f 100644 --- a/src/mol-geo/representation/volume/index.ts +++ b/src/mol-geo/representation/volume/index.ts @@ -8,21 +8,19 @@ import { Task } from 'mol-task' import { RenderObject } from 'mol-gl/render-object'; import { RepresentationProps, Representation } from '..'; import { VolumeData } from 'mol-model/volume'; -import { PickingId, PickingInfo } from '../../util/picking'; +import { PickingId } from '../../util/picking'; +import { Loci } from 'mol-model/loci'; +import { FlagAction } from '../../util/flag-data'; export interface VolumeElementRepresentation<P> { renderObjects: ReadonlyArray<RenderObject> create: (volumeData: VolumeData, props: P) => Task<void> update: (props: P) => Task<boolean> - getLabel: (pickingId: PickingId) => PickingInfo | null + getLoci: (pickingId: PickingId) => Loci | null + applyFlags: (loci: Loci, action: FlagAction) => void } -export interface VolumeRepresentation<P extends RepresentationProps = {}> extends Representation<VolumeData, P> { - renderObjects: ReadonlyArray<RenderObject> - create: (volumeData: VolumeData, props?: P) => Task<void> - update: (props: P) => Task<void> - getLabel: (pickingId: PickingId) => PickingInfo | null -} +export interface VolumeRepresentation<P extends RepresentationProps = {}> extends Representation<VolumeData, P> { } export function VolumeRepresentation<P>(reprCtor: () => VolumeElementRepresentation<P>): VolumeRepresentation<P> { const renderObjects: RenderObject[] = [] @@ -39,8 +37,12 @@ export function VolumeRepresentation<P>(reprCtor: () => VolumeElementRepresentat update(props: P) { return Task.create('VolumeRepresentation.update', async ctx => {}) }, - getLabel(pickingId: PickingId) { + getLoci(pickingId: PickingId) { + // TODO return null + }, + applyFlags(loci: Loci, action: FlagAction) { + // TODO } } } \ No newline at end of file diff --git a/src/mol-geo/representation/volume/surface.ts b/src/mol-geo/representation/volume/surface.ts index fab4c945407a158c3383537f9c1c8af0acf283d6..5bed7c8b5b05d250f6c3c99c1b8538a02e426305 100644 --- a/src/mol-geo/representation/volume/surface.ts +++ b/src/mol-geo/representation/volume/surface.ts @@ -18,7 +18,8 @@ import { createUniformColor } from '../../util/color-data'; import { getMeshData } from '../../util/mesh-data'; import { RenderableState, MeshValues } from 'mol-gl/renderable'; import { PickingId } from '../../util/picking'; -import { createEmptyFlags } from '../structure/utils'; +import { createEmptyFlags, FlagAction } from '../../util/flag-data'; +import { Loci } from 'mol-model/loci'; export function computeVolumeSurface(volume: VolumeData, isoValue: VolumeIsoValue) { return Task.create<Mesh>('Volume Surface', async ctx => { @@ -104,8 +105,12 @@ export default function Surface(): VolumeElementRepresentation<SurfaceProps> { return false }) }, - getLabel(pickingId: PickingId) { + getLoci(pickingId: PickingId) { + // TODO return null + }, + applyFlags(loci: Loci, action: FlagAction) { + // TODO } } } diff --git a/src/mol-geo/util/flag-data.ts b/src/mol-geo/util/flag-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8049a53ef546b320919a804ed9b40df46a6b764 --- /dev/null +++ b/src/mol-geo/util/flag-data.ts @@ -0,0 +1,102 @@ +/** + * 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 { Vec2 } from 'mol-math/linear-algebra' +import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; + +export type FlagData = { + tFlag: ValueCell<TextureImage> + uFlagTexSize: ValueCell<Vec2> +} + +export enum FlagAction { + Highlight, + RemoveHighlight, + Select, + Deselect, + ToggleSelect, + Clear +} + +export function applyFlagAction(array: Uint8Array, start: number, end: number, action: FlagAction) { + let changed = false + for (let i = start; i < end; ++i) { + let v = array[i] + switch (action) { + case FlagAction.Highlight: + if (v % 2 === 0) { + v += 1 + changed = true + } + break + case FlagAction.RemoveHighlight: + if (v % 2 !== 0) { + v -= 1 + changed = true + } + break + case FlagAction.Select: + v += 2 + changed = true + break + case FlagAction.Deselect: + if (v >= 2) { + v -= 2 + changed = true + } + break + case FlagAction.ToggleSelect: + if (v === 0) { + v = 2 + } else if (v === 1) { + v = 3 + } else if (v === 2) { + v = 0 + } else { + v -= 2 + } + changed = true + break + case FlagAction.Clear: + v = 0 + changed = true + break + } + array[i] = v + } + return changed +} + +export function createFlags(count: number, flagData?: FlagData): FlagData { + const flags = flagData && flagData.tFlag.ref.value.array.length >= count + ? flagData.tFlag.ref.value + : createTextureImage(count, 1) + if (flagData) { + ValueCell.update(flagData.tFlag, flags) + ValueCell.update(flagData.uFlagTexSize, Vec2.create(flags.width, flags.height)) + return flagData + } else { + return { + tFlag: ValueCell.create(flags), + uFlagTexSize: ValueCell.create(Vec2.create(flags.width, flags.height)), + } + } +} + +const emptyFlagTexture = { array: new Uint8Array(1), width: 1, height: 1 } +export function createEmptyFlags(flagData?: FlagData) { + if (flagData) { + ValueCell.update(flagData.tFlag, emptyFlagTexture) + ValueCell.update(flagData.uFlagTexSize, Vec2.create(1, 1)) + return flagData + } else { + return { + tFlag: ValueCell.create(emptyFlagTexture), + uFlagTexSize: ValueCell.create(Vec2.create(1, 1)), + } + } +} \ No newline at end of file diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts index 2b57d415255f4c8220d6520c4a4810430a89c8fb..0d087d672c95cb91dffe70c523355b8e98db2c28 100644 --- a/src/mol-gl/_spec/renderer.spec.ts +++ b/src/mol-gl/_spec/renderer.spec.ts @@ -19,7 +19,7 @@ import { RenderableState } from '../renderable'; import { createPointRenderObject } from '../render-object'; import { PointValues } from '../renderable/point'; import Scene from '../scene'; -import { createEmptyFlags } from 'mol-geo/representation/structure/utils'; +import { createEmptyFlags } from 'mol-geo/util/flag-data'; // function writeImage(gl: WebGLRenderingContext, width: number, height: number) { // const pixels = new Uint8Array(width * height * 4) diff --git a/src/mol-gl/shader/mesh.frag b/src/mol-gl/shader/mesh.frag index 401a0e46bc5aa9f1d6db723aeebe117e5846b38a..6350ea200a8ba7b950f44554b53a54a2bb03f702 100644 --- a/src/mol-gl/shader/mesh.frag +++ b/src/mol-gl/shader/mesh.frag @@ -76,8 +76,20 @@ void main() { gl_FragColor.rgb = finalColor; gl_FragColor.a = uAlpha; - if (vFlag == 1.0) { + // if (vFlag == 1.0) { + // gl_FragColor.rgb = mix(vec3(1.0, 0.4, 0.6), gl_FragColor.rgb, 0.3); + // } + + float flag = floor(vFlag * 255.0); + + if (flag == 0.0) { + // diffuseColor = vec4( vColor, opacity ); + } else if (mod(flag, 2.0) == 0.0) { + // diffuseColor = vec4(highlightColor, opacity); gl_FragColor.rgb = mix(vec3(1.0, 0.4, 0.6), gl_FragColor.rgb, 0.3); + } else { + // diffuseColor = vec4(selectionColor, opacity); + gl_FragColor.rgb = mix(vec3(0.2, 1.0, 0.1), gl_FragColor.rgb, 0.3); } #endif } \ No newline at end of file diff --git a/src/mol-math/graph/int-graph.ts b/src/mol-math/graph/int-graph.ts index f6576942cee2afd79ec9315a9e55ade52a301780..601196214baae16264b22823d262b0e33e9879b5 100644 --- a/src/mol-math/graph/int-graph.ts +++ b/src/mol-math/graph/int-graph.ts @@ -88,7 +88,7 @@ namespace IntGraph { * builder.addNextEdge(); * builder.assignProperty(property, srcProp[i]); * } - * return builder.createGraph({ property }); + * return builder.createGraph({ property }); */ addNextEdge() { const a = this.xs[this.current], b = this.ys[this.current]; diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d10cb93063593a236d649b6a2911b492c00d300 --- /dev/null +++ b/src/mol-model/loci.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Element } from './structure' +import { Bond } from './structure/structure/unit/bonds' + +/** A Loci that includes every loci */ +export const EveryLoci = { kind: 'every-loci' as 'every-loci' } +export type EveryLoci = typeof EveryLoci +export function isEveryLoci(x: any): x is EveryLoci { + return !!x && x.kind === 'every-loci'; +} + +export type Loci = Element.Loci | Bond.Loci | EveryLoci \ No newline at end of file diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 270fdcdf764fa6ee1174c3b539f57243c300a997..af6a4cdb275202f29a794c3a4803a0d60359bd2b 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ @@ -23,7 +23,11 @@ namespace Element { export function createEmptyArray(n: number): Element[] { return new Float64Array(n) as any; } /** All the information required to access element properties */ - export interface Location { unit: Unit, element: number } + export interface Location { + unit: Unit, + /** Index into element (atomic/coarse) properties of unit.model */ + element: number + } export function Location(unit?: Unit, element?: number): Location { return { unit: unit as any, element: element || 0 }; } export interface Property<T> { (location: Location): T } export interface Predicate extends Property<boolean> { } @@ -36,13 +40,18 @@ namespace Element { export function property<T>(p: Property<T>) { return p; } - /** Represents multiple element locations */ + /** Represents multiple element index locations */ export interface Loci { readonly kind: 'element-loci', - readonly elements: ReadonlyArray<{ unit: Unit, elements: SortedArray }> + /** Access i-th element as unit.elements[indices[i]] */ + readonly elements: ReadonlyArray<{ + unit: Unit, + /** Indices into the unit.elements array */ + indices: SortedArray + }> } - export function Loci(elements: ArrayLike<{ unit: Unit, elements: SortedArray }>): Loci { + export function Loci(elements: ArrayLike<{ unit: Unit, indices: SortedArray }>): Loci { return { kind: 'element-loci', elements: elements as Loci['elements'] }; } diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts index f90d51272746dc718f363f69dc337c706cd3b0b7..e5ef8d9080ebf11d6b2b5b6e6f2a76353aee9a61 100644 --- a/src/mol-model/structure/structure/unit.ts +++ b/src/mol-model/structure/structure/unit.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ @@ -34,9 +34,17 @@ namespace Unit { } } - // A group of units that differ only by symmetry operators. + /** A group of units that differ only by symmetry operators. */ export type SymmetryGroup = { readonly elements: SortedArray, readonly units: ReadonlyArray<Unit> } + /** Find index of unit with given id, returns -1 if not found */ + export function findUnitById(id: number, units: ReadonlyArray<Unit>) { + for (let i = 0, il = units.length; i < il; ++i) { + if (units[i].id === id) return i + } + return -1 + } + export interface Base { readonly id: number, // invariant ID stays the same even if the Operator/conformation changes. diff --git a/src/mol-model/structure/structure/unit/bonds.ts b/src/mol-model/structure/structure/unit/bonds.ts index 73b6ff8d67893590bf5babc999bd04fd60c9118d..d7268742832c8d7ff04d19199e218655ff48a099 100644 --- a/src/mol-model/structure/structure/unit/bonds.ts +++ b/src/mol-model/structure/structure/unit/bonds.ts @@ -4,23 +4,27 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Element } from '../../structure' +import { Unit } from '../../structure' export * from './bonds/intra-data' export * from './bonds/intra-compute' -interface Bond { - readonly a: Readonly<Element.Location>, - readonly b: Readonly<Element.Location> -} - namespace Bond { + export interface Location { + readonly aUnit: Unit, + /** Index into aUnit.elements */ + readonly aIndex: number, + readonly bUnit: Unit, + /** Index into bUnit.elements */ + readonly bIndex: number, + } + export interface Loci { readonly kind: 'bond-loci', - readonly bonds: ReadonlyArray<Bond> + readonly bonds: ReadonlyArray<Location> } - export function Loci(bonds: ArrayLike<Bond>): Loci { + export function Loci(bonds: ArrayLike<Location>): Loci { return { kind: 'bond-loci', bonds: bonds as Loci['bonds'] }; } diff --git a/src/mol-view/label.ts b/src/mol-view/label.ts new file mode 100644 index 0000000000000000000000000000000000000000..578a4a7a53260c5c682edc6a7d648edf692a0031 --- /dev/null +++ b/src/mol-view/label.ts @@ -0,0 +1,65 @@ +/** + * 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, Element, Queries } from 'mol-model/structure'; +import { Bond } from 'mol-model/structure/structure/unit/bonds'; +import { Loci } from 'mol-model/loci'; + +const elementLocA = Element.Location() +const elementLocB = Element.Location() + +function setElementLocation(loc: Element.Location, unit: Unit, index: number) { + loc.unit = unit + loc.element = unit.elements[index] +} + +export function labelFirst(loci: Loci) { + if(Element.isLoci(loci)) { + const e = loci.elements[0] + if (e && e.indices[0] !== undefined) { + return elementLabel(Element.Location(e.unit, e.indices[0])) + } + } else if (Bond.isLoci(loci)) { + const bond = loci.bonds[0] + if (bond) { + setElementLocation(elementLocA, bond.aUnit, bond.aIndex) + setElementLocation(elementLocB, bond.bUnit, bond.bIndex) + return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}` + } + } + return '' +} + +export function elementLabel(loc: Element.Location) { + const model = loc.unit.model.label + const instance = loc.unit.conformation.operator.name + let element = '' + + if (Unit.isAtomic(loc.unit)) { + const asym_id = Queries.props.chain.auth_asym_id(loc) + const seq_id = Queries.props.residue.auth_seq_id(loc) + const comp_id = Queries.props.residue.auth_comp_id(loc) + const atom_id = Queries.props.atom.auth_atom_id(loc) + element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}` + } else if (Unit.isCoarse(loc.unit)) { + const asym_id = Queries.props.coarse.asym_id(loc) + const seq_id_begin = Queries.props.coarse.seq_id_begin(loc) + const seq_id_end = Queries.props.coarse.seq_id_end(loc) + if (seq_id_begin === seq_id_end) { + const entityKey = Queries.props.coarse.entityKey(loc) + const seq = loc.unit.model.sequence.byEntityKey[entityKey] + const comp_id = seq.compId.value(seq_id_begin) + element = `[${comp_id}]${seq_id_begin}:${asym_id}` + } else { + element = `${seq_id_begin}-${seq_id_end}:${asym_id}` + } + } else { + element = 'unknown' + } + + return `${model} ${instance} ${element}` +} \ No newline at end of file diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 1992d98de3ddf92c037d8119f73eb32cf516be96..af44bda4bf3e7f202db99f95f6f971f8ef7b347b 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -7,7 +7,7 @@ import Viewer from 'mol-view/viewer' import { StateContext } from './state/context'; import { Progress } from 'mol-task'; -import { MmcifUrlToSpacefill } from './state/transform'; +import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBond } from './state/transform'; import { UrlEntity } from './state/entity'; import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; @@ -42,9 +42,12 @@ export class Stage { this.loadMmcifUrl(`../../examples/1cbs_full.bcif`) } - loadMmcifUrl (url: string) { + async loadMmcifUrl (url: string) { const urlEntity = UrlEntity.ofUrl(this.ctx, url) - MmcifUrlToSpacefill.apply(this.ctx, urlEntity, spacefillProps) + const modelEntity = await MmcifUrlToModel.apply(this.ctx, urlEntity) + const structureEntity = await ModelToStructure.apply(this.ctx, modelEntity) + StructureToSpacefill.apply(this.ctx, structureEntity, spacefillProps) + StructureToBond.apply(this.ctx, structureEntity, spacefillProps) // TODO props } loadPdbid (pdbid: string) { diff --git a/src/mol-view/state/entity.ts b/src/mol-view/state/entity.ts index 2b28f71d3d8cda0dc347f3d0501d3f4311d41ee9..a35ec66145f42af1a91551db8412450ad106a764 100644 --- a/src/mol-view/state/entity.ts +++ b/src/mol-view/state/entity.ts @@ -13,6 +13,7 @@ import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'; import { Model, Structure } from 'mol-model/structure'; import { StructureRepresentation } from 'mol-geo/representation/structure'; import { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; +import { BondProps } from 'mol-geo/representation/structure/bond'; const getNextId = idFactory(1) @@ -119,4 +120,11 @@ export namespace SpacefillEntity { export function ofRepr(ctx: StateContext, repr: StructureRepresentation<SpacefillProps>): SpacefillEntity { return StateEntity.create(ctx, 'spacefill', repr ) } +} + +export type BondEntity = StateEntity<StructureRepresentation<BondProps>, 'bond'> +export namespace BondEntity { + export function ofRepr(ctx: StateContext, repr: StructureRepresentation<BondProps>): BondEntity { + return StateEntity.create(ctx, 'bond', repr ) + } } \ No newline at end of file diff --git a/src/mol-view/state/transform.ts b/src/mol-view/state/transform.ts index f7e16db18dab957787eea9d881c8a39df7d32983..ed4adbc48df82f0ac8af9be9720dd2bd5eeda63f 100644 --- a/src/mol-view/state/transform.ts +++ b/src/mol-view/state/transform.ts @@ -5,13 +5,14 @@ */ import CIF from 'mol-io/reader/cif' -import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity } from './entity'; +import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BondEntity } from './entity'; import { Model, Structure } from 'mol-model/structure'; import { StateContext } from './context'; import Spacefill, { SpacefillProps } from 'mol-geo/representation/structure/spacefill'; import { StructureRepresentation } from 'mol-geo/representation/structure'; import StructureSymmetry from 'mol-model/structure/structure/symmetry'; +import Bond, { BondProps } from 'mol-geo/representation/structure/bond'; type transformer<I extends AnyEntity, O extends AnyEntity, P extends {}> = (ctx: StateContext, inputEntity: I, props?: P) => Promise<O> @@ -98,30 +99,42 @@ export const StructureToSpacefill: StructureToSpacefill = StateTransform.create( ctx.viewer.add(spacefillRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) - // ctx.viewer.input.drag.subscribe(async () => { - // console.log('drag') - // console.time('spacefill update') - // await spacefillRepr.update(props).run(ctx.log) - // console.timeEnd('spacefill update') - // ctx.viewer.add(spacefillRepr) - // ctx.viewer.update() - // ctx.viewer.requestDraw() - // }) return SpacefillEntity.ofRepr(ctx, spacefillRepr) }) +export type StructureToBond = StateTransform<StructureEntity, BondEntity, BondProps> + export const StructureToBond: StructureToBond = StateTransform.create('structure', 'bond', 'structure-to-bond', + async function (ctx: StateContext, structureEntity: StructureEntity, props: BondProps = {}) { + const bondRepr = StructureRepresentation(Bond) + await bondRepr.create(structureEntity.value, props).run(ctx.log) + ctx.viewer.add(bondRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return BondEntity.ofRepr(ctx, bondRepr) + }) + export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, SpacefillProps> export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update', async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: SpacefillProps = {}) { const spacefillRepr = spacefillEntity.value await spacefillRepr.update(props).run(ctx.log) ctx.viewer.add(spacefillRepr) - // ctx.viewer.update() ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) return NullEntity }) +export type BondUpdate = StateTransform<BondEntity, NullEntity, BondProps> + export const BondUpdate: BondUpdate = StateTransform.create('bond', 'null', 'bond-update', + async function (ctx: StateContext, bondEntity: BondEntity, props: BondProps = {}) { + const bondRepr = bondEntity.value + await bondRepr.update(props).run(ctx.log) + ctx.viewer.add(bondRepr) + ctx.viewer.requestDraw() + console.log('stats', ctx.viewer.stats) + return NullEntity + }) + // composed export type MmcifUrlToModel = StateTransform<UrlEntity, ModelEntity, {}> @@ -150,6 +163,7 @@ export type ModelToSpacefill = StateTransform<ModelEntity, SpacefillEntity, Spac export const ModelToSpacefill: ModelToSpacefill = StateTransform.create('model', 'spacefill', 'model-to-spacefill', async function (ctx: StateContext, modelEntity: ModelEntity, props: SpacefillProps = {}) { const structureEntity = await ModelToStructure.apply(ctx, modelEntity) + // StructureToBond.apply(ctx, structureEntity, props) return StructureToSpacefill.apply(ctx, structureEntity, props) }) diff --git a/src/mol-view/viewer.ts b/src/mol-view/viewer.ts index e2a6dc57ef38e03d0279dddcebdc749afb257d0f..06671bcfe105e537b5a6f21692b227879ca1c13b 100644 --- a/src/mol-view/viewer.ts +++ b/src/mol-view/viewer.ts @@ -22,6 +22,9 @@ import { createRenderTarget } from 'mol-gl/webgl/render-target'; import Scene from 'mol-gl/scene'; import { RenderVariant } from 'mol-gl/webgl/render-item'; import { PickingId, decodeIdRGBA } from 'mol-geo/util/picking'; +import { labelFirst } from './label'; +import { FlagAction } from 'mol-geo/util/flag-data'; +import { EveryLoci } from 'mol-model/loci'; interface Viewer { center: (p: Vec3) => void @@ -80,14 +83,27 @@ namespace Viewer { const p = identify(x, y) let label = '' reprMap.forEach((roSet, repr) => { - const info = repr.getLabel(p) - if (info) label = info.label - repr.update({ hoverSelection: p }).run().then(() => { + repr.applyFlags(EveryLoci, FlagAction.RemoveHighlight) + const loci = repr.getLoci(p) + if (loci) { + label = labelFirst(loci) + repr.applyFlags(loci, FlagAction.Highlight) + } + scene.update() + requestDraw() + }) + identified.next(`Object: ${p.objectId}, Instance: ${p.instanceId}, Element: ${p.elementId}, Label: ${label}`) + }) + input.click.subscribe(({x, y}) => { + const p = identify(x, y) + reprMap.forEach((roSet, repr) => { + const loci = repr.getLoci(p) + if (loci) { + repr.applyFlags(loci, FlagAction.ToggleSelect) scene.update() requestDraw() - }) + } }) - identified.next(`Object: ${p.objectId}, Instance: ${p.instanceId}, Element: ${p.elementId}, Label: ${label}`) }) const camera = PerspectiveCamera.create({