Browse Source

Merge branch 'master' of https://github.com/molstar/molstar-proto into mol-model

David Sehnal 6 years ago
parent
commit
c837b827b3

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

+ 194 - 0
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>;
+    }
+}

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

+ 6 - 3
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
 }

+ 211 - 0
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)
+            }
+        }
+    }
+}

+ 15 - 48
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)
+            }
         }
     }
 }

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

+ 18 - 22
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)
         }
     }
 }

+ 34 - 49
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)
+    }
+}

+ 11 - 9
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
         }
     }
 }

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

+ 102 - 0
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)),
+        }
+    }
+}

+ 1 - 1
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)

+ 13 - 1
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
 }

+ 1 - 1
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];

+ 17 - 0
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

+ 14 - 5
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'] };
     }
 

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

+ 12 - 8
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'] };
     }
 

+ 65 - 0
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}`
+}

+ 6 - 3
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) {

+ 8 - 0
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 )
+    }
 }

+ 25 - 11
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)
     })
 

+ 21 - 5
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({