Browse Source

wip, cartoon/backbone repr

Alexander Rose 6 years ago
parent
commit
c19474cd07
37 changed files with 1739 additions and 229 deletions
  1. 7 0
      src/apps/schema-generator/util/cif-dic.ts
  2. 10 1
      src/mol-app/ui/entity/tree.tsx
  3. 243 0
      src/mol-app/ui/transform/backbone.tsx
  4. 3 3
      src/mol-app/ui/transform/ball-and-stick.tsx
  5. 243 0
      src/mol-app/ui/transform/cartoon.tsx
  6. 234 0
      src/mol-app/ui/transform/distance-restraint.tsx
  7. 9 0
      src/mol-app/ui/transform/list.tsx
  8. 1 1
      src/mol-geo/representation/index.ts
  9. 52 0
      src/mol-geo/representation/structure/backbone.ts
  10. 52 0
      src/mol-geo/representation/structure/cartoon.ts
  11. 3 7
      src/mol-geo/representation/structure/index.ts
  12. 9 19
      src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
  13. 11 43
      src/mol-geo/representation/structure/visual/element-point.ts
  14. 15 31
      src/mol-geo/representation/structure/visual/element-sphere.ts
  15. 9 18
      src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
  16. 9 18
      src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
  17. 207 0
      src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
  18. 0 0
      src/mol-geo/representation/structure/visual/polymer-backbone-sphere.ts
  19. 207 0
      src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
  20. 1 3
      src/mol-geo/representation/structure/visual/util/common.ts
  21. 13 3
      src/mol-geo/representation/structure/visual/util/element.ts
  22. 93 0
      src/mol-geo/representation/structure/visual/util/polymer.ts
  23. 3 7
      src/mol-geo/representation/volume/index.ts
  24. 4 7
      src/mol-geo/representation/volume/surface.ts
  25. 1 1
      src/mol-io/reader/cif/schema/mmcif.ts
  26. 1 0
      src/mol-model/structure/export/mmcif.ts
  27. 28 2
      src/mol-model/structure/model/formats/mmcif.ts
  28. 8 3
      src/mol-model/structure/model/formats/mmcif/atomic.ts
  29. 13 5
      src/mol-model/structure/model/formats/mmcif/ihm.ts
  30. 5 2
      src/mol-model/structure/model/model.ts
  31. 6 1
      src/mol-model/structure/model/properties/atomic/hierarchy.ts
  32. 17 0
      src/mol-model/structure/model/properties/chemical-component.ts
  33. 6 1
      src/mol-model/structure/model/properties/coarse/hierarchy.ts
  34. 120 46
      src/mol-model/structure/model/types.ts
  35. 34 6
      src/mol-view/stage.ts
  36. 16 0
      src/mol-view/state/entity.ts
  37. 46 1
      src/mol-view/state/transform.ts

+ 7 - 0
src/apps/schema-generator/util/cif-dic.ts

@@ -178,6 +178,10 @@ const SPACE_SEPARATED_LIST_FIELDS = [
     '_pdbx_soln_scatter.data_analysis_software_list', // SCTPL5 GNOM
 ];
 
+const SEMICOLON_SEPARATED_LIST_FIELDS = [
+    '_chem_comp.pdbx_synonyms' // GLYCERIN; PROPANE-1,2,3-TRIOL
+]
+
 export function generateSchema (frames: CifFrame[]) {
     const schema: Database = {}
 
@@ -250,6 +254,9 @@ export function generateSchema (frames: CifFrame[]) {
                         } else if (SPACE_SEPARATED_LIST_FIELDS.includes(d.header)) {
                             fieldType = { 'list': [ 'str', ' ' ] };
                             console.log(`space separated: ${d.header}`)
+                        } else if (SEMICOLON_SEPARATED_LIST_FIELDS.includes(d.header)) {
+                            fieldType = { 'list': [ 'str', ';' ] };
+                            console.log(`space separated: ${d.header}`)
                         }
                     }
                     fields[itemName] = fieldType

+ 10 - 1
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, StructureToBallAndStick, BallAndStickUpdate } from 'mol-view/state/transform';
+import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill, StructureCenter, StructureToBallAndStick, DistanceRestraintUpdate, CartoonUpdate, BallAndStickUpdate, BackboneUpdate } from 'mol-view/state/transform';
 
 function getTransforms(entity: AnyEntity): AnyTransform[] {
     const transforms: AnyTransform[] = []
@@ -48,6 +48,15 @@ function getTransforms(entity: AnyEntity): AnyTransform[] {
         case 'ballandstick':
             transforms.push(BallAndStickUpdate)
             break;
+        case 'distancerestraint':
+            transforms.push(DistanceRestraintUpdate)
+            break;
+        case 'backbone':
+            transforms.push(BackboneUpdate)
+            break;
+        case 'cartoon':
+            transforms.push(CartoonUpdate)
+            break;
     }
     return transforms
 }

+ 243 - 0
src/mol-app/ui/transform/backbone.tsx

@@ -0,0 +1,243 @@
+/**
+ * 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 { BackboneEntity } from 'mol-view/state/entity';
+import { BackboneUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface BackboneState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    detail: number
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
+}
+
+export class Backbone extends View<Controller<any>, BackboneState, { transform: BackboneUpdate, entity: BackboneEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<BackboneState>) {
+        console.log(state)
+        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 qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
+            return <option key={value} value={value}>{value.toString()}</option>
+        })
+
+        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>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Sphere detail</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.detail}
+                                    onChange={(e) => this.update({ detail: parseInt(e.target.value) })}
+                                >
+                                    {sphereDetailOptions}
+                                </select>
+                            </div>
+                        </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 - 3
src/mol-app/ui/transform/ball-and-stick.tsx

@@ -12,8 +12,8 @@ import * as React from 'react'
 import { View } from '../view';
 import { Controller } from '../../controller/controller';
 import { Toggle } from '../controls/common';
-import { BallAndStickEntity } from 'mol-view/state/entity';
-import { BallAndStickUpdate } from 'mol-view/state/transform'
+import { DistanceRestraintEntity } from 'mol-view/state/entity';
+import { DistanceRestraintUpdate } from 'mol-view/state/transform'
 import { StateContext } from 'mol-view/state/context';
 import { ColorTheme, SizeTheme } from 'mol-geo/theme';
 import { Color, ColorNames } from 'mol-util/color';
@@ -50,7 +50,7 @@ interface BallAndStickState {
     unitKinds: Unit.Kind[]
 }
 
-export class BallAndStick extends View<Controller<any>, BallAndStickState, { transform: BallAndStickUpdate, entity: BallAndStickEntity, ctx: StateContext }> {
+export class BallAndStick extends View<Controller<any>, BallAndStickState, { transform: DistanceRestraintUpdate, entity: DistanceRestraintEntity, ctx: StateContext }> {
     state = {
         doubleSided: true,
         flipSided: false,

+ 243 - 0
src/mol-app/ui/transform/cartoon.tsx

@@ -0,0 +1,243 @@
+/**
+ * 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 { CartoonEntity } from 'mol-view/state/entity';
+import { CartoonUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface CartoonState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    detail: number
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
+}
+
+export class Cartoon extends View<Controller<any>, CartoonState, { transform: CartoonUpdate, entity: CartoonEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<CartoonState>) {
+        console.log(state)
+        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 qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
+            return <option key={value} value={value}>{value.toString()}</option>
+        })
+
+        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>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Sphere detail</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.detail}
+                                    onChange={(e) => this.update({ detail: parseInt(e.target.value) })}
+                                >
+                                    {sphereDetailOptions}
+                                </select>
+                            </div>
+                        </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>;
+    }
+}

+ 234 - 0
src/mol-app/ui/transform/distance-restraint.tsx

@@ -0,0 +1,234 @@
+/**
+ * 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 { DistanceRestraintEntity } from 'mol-view/state/entity';
+import { DistanceRestraintUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface DistanceRestraintState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    linkScale: number
+    linkSpacing: number
+    linkRadius: number
+    radialSegments: number
+    detail: number
+    unitKinds: Unit.Kind[]
+}
+
+export class DistanceRestraint extends View<Controller<any>, DistanceRestraintState, { transform: DistanceRestraintUpdate, entity: DistanceRestraintEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        linkScale: 0.4,
+        linkSpacing: 1,
+        linkRadius: 0.25,
+        radialSegments: 16,
+        detail: 1,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<DistanceRestraintState>) {
+        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 qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        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>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </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>;
+    }
+}

+ 9 - 0
src/mol-app/ui/transform/list.tsx

@@ -19,6 +19,9 @@ import { AnyEntity } from 'mol-view/state/entity';
 import { FileLoader } from './file-loader';
 import { ModelToStructure } from './model';
 import { StructureCenter } from './structure';
+import { Cartoon } from './cartoon';
+import { DistanceRestraint } from './distance-restraint';
+import { Backbone } from './backbone';
 
 function getTransformComponent(controller: TransformListController, entity: AnyEntity, transform: AnyTransform) {
     switch (transform.kind) {
@@ -32,6 +35,12 @@ function getTransformComponent(controller: TransformListController, entity: AnyE
             return <Spacefill controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Spacefill>
         case 'ballandstick-update':
             return <BallAndStick controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></BallAndStick>
+        case 'distancerestraint-update':
+            return <DistanceRestraint controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></DistanceRestraint>
+        case 'backbone-update':
+            return <Backbone controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Backbone>
+        case 'cartoon-update':
+            return <Cartoon controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Cartoon>
     }
     return <Transform controller={controller} entity={entity} transform={transform}></Transform>
 }

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

@@ -23,7 +23,7 @@ export interface Representation<D, P extends RepresentationProps = {}> {
 }
 
 export interface Visual<D, P extends RepresentationProps = {}> {
-    readonly renderObjects: ReadonlyArray<RenderObject>
+    readonly renderObject: RenderObject
     create: (ctx: RuntimeContext, data: D, props: P) => Promise<void>
     update: (ctx: RuntimeContext, props: P) => Promise<boolean>
     getLoci: (pickingId: PickingId) => Loci

+ 52 - 0
src/mol-geo/representation/structure/backbone.ts

@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, StructureUnitsRepresentation } from '.';
+import { PickingId } from '../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { PolymerBackboneVisual, DefaultPolymerBackboneProps } from './visual/polymer-backbone-cylinder';
+
+export const DefaultBackboneProps = {
+    ...DefaultPolymerBackboneProps
+}
+export type BackboneProps = Partial<typeof DefaultBackboneProps>
+
+export function BackboneRepresentation(): StructureRepresentation<BackboneProps> {
+    const traceRepr = StructureUnitsRepresentation(PolymerBackboneVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...traceRepr.renderObjects ]
+        },
+        get props() {
+            return { ...traceRepr.props }
+        },
+        create: (structure: Structure, props: BackboneProps = {} as BackboneProps) => {
+            const p = Object.assign({}, DefaultBackboneProps, props)
+            return Task.create('BackboneRepresentation', async ctx => {
+                await traceRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: BackboneProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating BackboneRepresentation', async ctx => {
+                await traceRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            return traceRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            traceRepr.mark(loci, action)
+        },
+        destroy() {
+            traceRepr.destroy()
+        }
+    }
+}

+ 52 - 0
src/mol-geo/representation/structure/cartoon.ts

@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, StructureUnitsRepresentation } from '.';
+import { PickingId } from '../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh';
+
+export const DefaultCartoonProps = {
+    ...DefaultPolymerTraceProps
+}
+export type CartoonProps = Partial<typeof DefaultCartoonProps>
+
+export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
+    const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...traceRepr.renderObjects ]
+        },
+        get props() {
+            return { ...traceRepr.props }
+        },
+        create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => {
+            const p = Object.assign({}, DefaultCartoonProps, props)
+            return Task.create('CartoonRepresentation', async ctx => {
+                await traceRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: CartoonProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating CartoonRepresentation', async ctx => {
+                await traceRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            return traceRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            traceRepr.mark(loci, action)
+        },
+        destroy() {
+            traceRepr.destroy()
+        }
+    }
+}

+ 3 - 7
src/mol-geo/representation/structure/index.ts

@@ -79,12 +79,8 @@ export function StructureRepresentation<P extends StructureProps>(visualCtor: ()
     }
 
     return {
-        get renderObjects() {
-            return visual.renderObjects
-        },
-        get props() {
-            return _props
-        },
+        get renderObjects() { return [ visual.renderObject ] },
+        get props() { return _props },
         create,
         update,
         getLoci,
@@ -185,7 +181,7 @@ export function StructureUnitsRepresentation<P extends StructureProps>(visualCto
     return {
         get renderObjects() {
             const renderObjects: RenderObject[] = []
-            visuals.forEach(({ visual }) => renderObjects.push(...visual.renderObjects))
+            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
             return renderObjects
         },
         get props() {

+ 9 - 19
src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts

@@ -6,7 +6,7 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
 import { Link, Structure } from 'mol-model/structure';
 import { DefaultStructureProps, StructureVisual } from '../index';
 import { RuntimeContext } from 'mol-task'
@@ -57,18 +57,15 @@ export const DefaultCrossLinkRestraintProps = {
 export type CrossLinkRestraintProps = Partial<typeof DefaultCrossLinkRestraintProps>
 
 export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintProps> {
-    const renderObjects: RenderObject[] = []
-    let cylinders: MeshRenderObject
+    let renderObject: MeshRenderObject
     let currentProps: typeof DefaultCrossLinkRestraintProps
     let mesh: Mesh
     let currentStructure: Structure
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, structure: Structure, props: CrossLinkRestraintProps = {}) {
             currentProps = Object.assign({}, DefaultCrossLinkRestraintProps, props)
-
-            renderObjects.length = 0 // clear
             currentStructure = structure
 
             const elementCount = structure.crossLinkRestraints.count
@@ -76,13 +73,8 @@ export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintPr
 
             mesh = await createCrossLinkRestraintCylinderMesh(ctx, structure, currentProps)
 
-            if (ctx.shouldUpdate) await ctx.update('Computing link transforms');
             const transforms = createIdentityTransform()
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link colors');
             const color = createUniformColor({ value: 0x119911 }) // TODO
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link marks');
             const marker = createMarkers(instanceCount * elementCount)
 
             const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
@@ -97,28 +89,26 @@ export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintPr
             }
             const state = createRenderableState(currentProps)
 
-            cylinders = createMeshRenderObject(values, state)
-            console.log(values, instanceCount, elementCount)
-            renderObjects.push(cylinders)
+            renderObject = createMeshRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: CrossLinkRestraintProps) {
             const newProps = Object.assign({}, currentProps, props)
 
-            if (!cylinders) return false
+            if (!renderObject) return false
 
             // TODO create in-place
             if (currentProps.radialSegments !== newProps.radialSegments) return false
 
-            updateMeshValues(cylinders.values, newProps)
-            updateRenderableState(cylinders.state, newProps)
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
 
             return false
         },
         getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentStructure, cylinders.id)
+            return getLinkLoci(pickingId, currentStructure, renderObject.id)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentStructure, cylinders.values)
+            markLink(loci, action, currentStructure, renderObject.values)
         },
         destroy() {
             // TODO

+ 11 - 43
src/mol-geo/representation/structure/visual/element-point.ts

@@ -6,21 +6,21 @@
  */
 
 import { ValueCell } from 'mol-util/value-cell'
-import { createPointRenderObject, RenderObject, PointRenderObject } from 'mol-gl/render-object'
-import { Unit, Element } from 'mol-model/structure';
+import { createPointRenderObject, PointRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
 import { RuntimeContext } from 'mol-task'
 import { fillSerial } from 'mol-gl/renderable/util';
 
 import { UnitsVisual, DefaultStructureProps } from '../index';
 import VertexMap from '../../../shape/vertex-map';
 import { SizeTheme } from '../../../theme';
-import { markElement } from './util/element';
+import { markElement, getElementLoci } from './util/element';
 import { createTransforms, createColors, createSizes } from './util/common';
 import { deepEqual, defaults } from 'mol-util';
-import { SortedArray, OrderedSet } from 'mol-data/int';
+import { SortedArray } from 'mol-data/int';
 import { RenderableState, PointValues } from 'mol-gl/renderable';
 import { PickingId } from '../../../util/picking';
-import { Loci, EmptyLoci } from 'mol-model/loci';
+import { Loci } from 'mol-model/loci';
 import { MarkerAction, createMarkers } from '../../../util/marker-data';
 import { Vec3 } from 'mol-math/linear-algebra';
 
@@ -49,8 +49,7 @@ export function createPointVertices(unit: Unit) {
 }
 
 export default function PointVisual(): UnitsVisual<PointProps> {
-    const renderObjects: RenderObject[] = []
-    let points: PointRenderObject
+    let renderObject: PointRenderObject
     let currentProps = DefaultPointProps
     let currentGroup: Unit.SymmetryGroup
 
@@ -58,11 +57,9 @@ export default function PointVisual(): UnitsVisual<PointProps> {
     let _elements: SortedArray
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PointProps = {}) {
             currentProps = Object.assign({}, DefaultPointProps, props)
-
-            renderObjects.length = 0 // clear
             currentGroup = group
 
             _units = group.units
@@ -79,19 +76,10 @@ export default function PointVisual(): UnitsVisual<PointProps> {
                 fillSerial(new Uint32Array(elementCount + 1))
             )
 
-            if (ctx.shouldUpdate) await ctx.update('Computing point vertices');
             const vertices = createPointVertices(_units[0])
-
-            if (ctx.shouldUpdate) await ctx.update('Computing point transforms');
             const transforms = createTransforms(group)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing point colors');
             const color = createColors(group, elementCount, colorTheme)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing point sizes');
             const size = createSizes(group, vertexMap, sizeTheme)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing spacefill marks');
             const marker = createMarkers(instanceCount * elementCount)
 
             const values: PointValues = {
@@ -118,11 +106,10 @@ export default function PointVisual(): UnitsVisual<PointProps> {
                 visible: defaults(props.visible, true)
             }
 
-            points = createPointRenderObject(values, state)
-            renderObjects.push(points)
+            renderObject = createPointRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: PointProps) {
-            if (!points || !_units || !_elements) return false
+            if (!renderObject || !_units || !_elements) return false
 
             const newProps = { ...currentProps, ...props }
             if (deepEqual(currentProps, newProps)) {
@@ -130,21 +117,8 @@ export default function PointVisual(): UnitsVisual<PointProps> {
                 return true
             }
 
-            // const elementCount = OrderedSet.size(_elementGroup.elements)
-            // const unitCount = _units.length
-
-            // const vertexMap = VertexMap.create(
-            //     elementCount,
-            //     elementCount + 1,
-            //     fillSerial(new Uint32Array(elementCount)),
-            //     fillSerial(new Uint32Array(elementCount + 1))
-            // )
-
             if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) {
                 console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme)
-                // if (ctx.shouldUpdate) await ctx.update('Computing point colors');
-                // const color = createColors(_units, _elementGroup, vertexMap, newProps.colorTheme)
-                // ValueCell.update(points.props.color, color)
             }
 
             if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) {
@@ -155,16 +129,10 @@ export default function PointVisual(): UnitsVisual<PointProps> {
             return false
         },
         getLoci(pickingId: PickingId) {
-            const { objectId, instanceId, elementId } = pickingId
-            if (points.id === objectId) {
-                const unit = currentGroup.units[instanceId]
-                const indices = OrderedSet.ofSingleton(elementId as Element.Index)
-                return Element.Loci([{ unit, indices }])
-            }
-            return EmptyLoci
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markElement(points.values.tMarker, currentGroup, loci, action)
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
         },
         destroy() {
             // TODO

+ 15 - 31
src/mol-geo/representation/structure/visual/element-sphere.ts

@@ -7,20 +7,19 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, Element } from 'mol-model/structure';
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
 import { DefaultStructureProps, UnitsVisual } from '../index';
 import { RuntimeContext } from 'mol-task'
 import { createTransforms, createColors } from '../visual/util/common';
-import { createElementSphereMesh, markElement, getElementRadius } from '../visual/util/element';
+import { createElementSphereMesh, markElement, getElementRadius, getElementLoci } from '../visual/util/element';
 import { deepEqual } from 'mol-util';
 import { MeshValues } from 'mol-gl/renderable';
 import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
-import { OrderedSet } from 'mol-data/int';
 import { createMarkers, MarkerAction } from '../../../util/marker-data';
-import { Loci, EmptyLoci } from 'mol-model/loci';
+import { Loci } from 'mol-model/loci';
 import { SizeTheme } from '../../../theme';
 import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 
@@ -34,18 +33,15 @@ export const DefaultElementSphereProps = {
 export type ElementSphereProps = Partial<typeof DefaultElementSphereProps>
 
 export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
-    const renderObjects: RenderObject[] = []
-    let spheres: MeshRenderObject
+    let renderObject: MeshRenderObject
     let currentProps: typeof DefaultElementSphereProps
     let mesh: Mesh
     let currentGroup: Unit.SymmetryGroup
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: ElementSphereProps = {}) {
             currentProps = Object.assign({}, DefaultElementSphereProps, props)
-
-            renderObjects.length = 0 // clear
             currentGroup = group
 
             const { detail, colorTheme, sizeTheme, unitKinds } = { ...DefaultElementSphereProps, ...props }
@@ -58,13 +54,8 @@ export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
                 ? await createElementSphereMesh(ctx, unit, radius, detail, mesh)
                 : Mesh.createEmpty(mesh)
 
-            if (ctx.shouldUpdate) await ctx.update('Computing spacefill transforms');
             const transforms = createTransforms(group)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing spacefill colors');
             const color = createColors(group, elementCount, colorTheme)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing spacefill marks');
             const marker = createMarkers(instanceCount * elementCount)
 
             const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
@@ -79,13 +70,12 @@ export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
             }
             const state = createRenderableState(currentProps)
 
-            spheres = createMeshRenderObject(values, state)
-            renderObjects.push(spheres)
+            renderObject = createMeshRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: ElementSphereProps) {
             const newProps = Object.assign({}, currentProps, props)
 
-            if (!spheres) return false
+            if (!renderObject) return false
 
             let updateColor = false
 
@@ -93,7 +83,7 @@ export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
                 const unit = currentGroup.units[0]
                 const radius = getElementRadius(unit, newProps.sizeTheme)
                 mesh = await createElementSphereMesh(ctx, unit, radius, newProps.detail, mesh)
-                ValueCell.update(spheres.values.drawCount, mesh.triangleCount * 3)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
                 updateColor = true
             }
 
@@ -103,27 +93,21 @@ export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
 
             if (updateColor) {
                 const elementCount = currentGroup.elements.length
-                if (ctx.shouldUpdate) await ctx.update('Computing spacefill colors');
-                createColors(currentGroup, elementCount, newProps.colorTheme, spheres.values)
+                if (ctx.shouldUpdate) await ctx.update('Computing sphere colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
             }
 
-            updateMeshValues(spheres.values, newProps)
-            updateRenderableState(spheres.state, newProps)
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
 
             currentProps = newProps
             return true
         },
         getLoci(pickingId: PickingId) {
-            const { objectId, instanceId, elementId } = pickingId
-            if (spheres.id === objectId) {
-                const unit = currentGroup.units[instanceId]
-                const indices = OrderedSet.ofSingleton(elementId as Element.Index);
-                return Element.Loci([{ unit, indices }])
-            }
-            return EmptyLoci
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markElement(spheres.values.tMarker, currentGroup, loci, action)
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
         },
         destroy() {
             // TODO

+ 9 - 18
src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts

@@ -6,7 +6,7 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
 import { Link, Structure } from 'mol-model/structure';
 import { DefaultStructureProps, StructureVisual } from '../index';
 import { RuntimeContext } from 'mol-task'
@@ -54,18 +54,15 @@ export const DefaultInterUnitLinkProps = {
 export type InterUnitLinkProps = Partial<typeof DefaultInterUnitLinkProps>
 
 export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
-    const renderObjects: RenderObject[] = []
-    let cylinders: MeshRenderObject
+    let renderObject: MeshRenderObject
     let currentProps: typeof DefaultInterUnitLinkProps
     let mesh: Mesh
     let currentStructure: Structure
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, structure: Structure, props: InterUnitLinkProps = {}) {
             currentProps = Object.assign({}, DefaultInterUnitLinkProps, props)
-
-            renderObjects.length = 0 // clear
             currentStructure = structure
 
             const elementCount = structure.links.bondCount
@@ -73,13 +70,8 @@ export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
 
             mesh = await createInterUnitLinkCylinderMesh(ctx, structure, currentProps)
 
-            if (ctx.shouldUpdate) await ctx.update('Computing link transforms');
             const transforms = createIdentityTransform()
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link colors');
             const color = createUniformColor({ value: 0x999911 }) // TODO
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link marks');
             const marker = createMarkers(instanceCount * elementCount)
 
             const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
@@ -94,27 +86,26 @@ export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
             }
             const state = createRenderableState(currentProps)
 
-            cylinders = createMeshRenderObject(values, state)
-            renderObjects.push(cylinders)
+            renderObject = createMeshRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: InterUnitLinkProps) {
             const newProps = Object.assign({}, currentProps, props)
 
-            if (!cylinders) return false
+            if (!renderObject) return false
 
             // TODO create in-place
             if (currentProps.radialSegments !== newProps.radialSegments) return false
 
-            updateMeshValues(cylinders.values, newProps)
-            updateRenderableState(cylinders.state, newProps)
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
 
             return false
         },
         getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentStructure, cylinders.id)
+            return getLinkLoci(pickingId, currentStructure, renderObject.id)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentStructure, cylinders.values)
+            markLink(loci, action, currentStructure, renderObject.values)
         },
         destroy() {
             // TODO

+ 9 - 18
src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts

@@ -7,7 +7,7 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
 import { Unit, Link } from 'mol-model/structure';
 import { UnitsVisual, DefaultStructureProps } from '../index';
 import { RuntimeContext } from 'mol-task'
@@ -70,18 +70,15 @@ export const DefaultIntraUnitLinkProps = {
 export type IntraUnitLinkProps = Partial<typeof DefaultIntraUnitLinkProps>
 
 export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
-    const renderObjects: RenderObject[] = []
-    let cylinders: MeshRenderObject
+    let renderObject: MeshRenderObject
     let currentProps: typeof DefaultIntraUnitLinkProps
     let mesh: Mesh
     let currentGroup: Unit.SymmetryGroup
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: IntraUnitLinkProps = {}) {
             currentProps = Object.assign({}, DefaultIntraUnitLinkProps, props)
-
-            renderObjects.length = 0 // clear
             currentGroup = group
 
             const unit = group.units[0]
@@ -90,13 +87,8 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
 
             mesh = await createIntraUnitLinkCylinderMesh(ctx, unit, currentProps)
 
-            if (ctx.shouldUpdate) await ctx.update('Computing link transforms');
             const transforms = createTransforms(group)
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link colors');
             const color = chainIdLinkColorData({ group, elementCount }) // TODO
-
-            if (ctx.shouldUpdate) await ctx.update('Computing link marks');
             const marker = createMarkers(instanceCount * elementCount)
 
             const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
@@ -111,27 +103,26 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
             }
             const state = createRenderableState(currentProps)
 
-            cylinders = createMeshRenderObject(values, state)
-            renderObjects.push(cylinders)
+            renderObject = createMeshRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: IntraUnitLinkProps) {
             const newProps = Object.assign({}, currentProps, props)
 
-            if (!cylinders) return false
+            if (!renderObject) return false
 
             // TODO create in-place
             if (currentProps.radialSegments !== newProps.radialSegments) return false
 
-            updateMeshValues(cylinders.values, newProps)
-            updateRenderableState(cylinders.state, newProps)
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
 
             return true
         },
         getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentGroup, cylinders.id)
+            return getLinkLoci(pickingId, currentGroup, renderObject.id)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentGroup, cylinders.values)
+            markLink(loci, action, currentGroup, renderObject.values)
         },
         destroy() {
             // TODO

+ 207 - 0
src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts

@@ -0,0 +1,207 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, Element, StructureProperties } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '../index';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from '../visual/util/common';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Segmentation } from 'mol-data/int';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { getPolymerElementCount } from './util/polymer';
+import { MoleculeType } from 'mol-model/structure/model/types';
+
+async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerElementCount = getPolymerElementCount(unit)
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+
+    const chemCompMap = unit.model.properties.chemicalComponentMap
+    const { elements } = unit
+    const curV = Vec3.zero()
+    const prevV = Vec3.zero()
+    const pos = unit.conformation.invariantPosition
+    const l = Element.Location(unit)
+
+    if (Unit.isAtomic(unit)) {
+        const { polymerSegments, residueSegments } = unit.model.atomicHierarchy
+        const polymerIt = Segmentation.transientSegments(polymerSegments, elements);
+        const residuesIt = Segmentation.transientSegments(residueSegments, elements);
+
+        let i = 0
+
+        while (polymerIt.hasNext) {
+            const polymerSegment = polymerIt.move();
+            residuesIt.setSegment(polymerSegment);
+            while (residuesIt.hasNext) {
+                const residueSegment = residuesIt.move();
+                l.element = elements[residueSegment.start];
+
+                const compId = StructureProperties.residue.label_comp_id(l)
+                const cc = chemCompMap.get(compId)
+                const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown
+                let traceName = ''
+                if (moleculeType === MoleculeType.protein) {
+                    traceName = 'CA'
+                } else if (moleculeType === MoleculeType.DNA || moleculeType === MoleculeType.RNA) {
+                    traceName = 'P'
+                }
+
+                for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
+                    l.element = elements[j];
+                    if (StructureProperties.atom.label_atom_id(l) === traceName) break
+                }
+                pos(l.element, curV)
+
+                builder.setId(residueSegment.start)
+                builder.addCylinder(prevV, curV, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+
+                Vec3.copy(prevV, curV)
+
+                if (i % 10000 === 0 && ctx.shouldUpdate) {
+                    await ctx.update({ message: 'Backbone mesh', current: i, max: polymerElementCount });
+                }
+                ++i
+            }
+        }
+    } else if (Unit.isSpheres(unit)) {
+        let prevSeqIdEnd = -1
+        for (let i = 0, il = elements.length; i < il; ++i) {
+            l.element = elements[i]
+            if (StructureProperties.entity.type(l) !== 'polymer') continue;
+
+            pos(elements[i], curV)
+            const seqIdBegin = StructureProperties.coarse.seq_id_begin(l)
+            const seqIdEnd = StructureProperties.coarse.seq_id_end(l)
+
+            pos(elements[i], curV)
+
+            if (seqIdBegin - 1 === prevSeqIdEnd) {
+                builder.setId(i)
+                builder.addCylinder(prevV, curV, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+            }
+
+            Vec3.copy(prevV, curV)
+            prevSeqIdEnd = seqIdEnd
+
+            if (i % 10000 === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'Backbone mesh', current: i, max: polymerElementCount });
+            }
+        }
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerBackboneProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerBackboneProps = Partial<typeof DefaultPolymerBackboneProps>
+
+export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerBackboneProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerBackboneProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerBackboneProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerBackboneProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerBackboneCylinderMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+
+            if (ctx.shouldUpdate) await ctx.update('Computing trace transforms');
+            const transforms = createTransforms(group)
+
+            if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+            const color = createColors(group, elementCount, colorTheme)
+
+            if (ctx.shouldUpdate) await ctx.update('Computing trace marks');
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerBackboneProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerBackboneCylinderMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            // TODO
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            // TODO
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 0 - 0
src/mol-geo/representation/structure/visual/polymer-backbone-sphere.ts


+ 207 - 0
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -0,0 +1,207 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, Element, StructureProperties } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '../index';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from '../visual/util/common';
+import { markElement } from '../visual/util/element';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { OrderedSet, Segmentation } from 'mol-data/int';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { getPolymerElementCount } from './util/polymer';
+
+async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerElementCount = getPolymerElementCount(unit)
+    console.log('polymerElementCount', polymerElementCount)
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+
+    const { elements } = unit
+    const curV = Vec3.zero()
+    const prevV = Vec3.zero()
+    const pos = unit.conformation.invariantPosition
+    const l = Element.Location(unit)
+
+    if (Unit.isAtomic(unit)) {
+        const { chainSegments, residueSegments } = unit.model.atomicHierarchy
+        const chainsIt = Segmentation.transientSegments(chainSegments, elements);
+        const residuesIt = Segmentation.transientSegments(residueSegments, elements);
+
+        let i = 0
+        let prevSeqId = -1
+
+        while (chainsIt.hasNext) {
+            const chainSegment = chainsIt.move();
+            residuesIt.setSegment(chainSegment);
+            while (residuesIt.hasNext) {
+                const residueSegment = residuesIt.move();
+                l.element = elements[residueSegment.start];
+                if (StructureProperties.entity.type(l) !== 'polymer') continue;
+
+                const seqId = StructureProperties.residue.label_seq_id(l)
+
+                // for (let j = residueSegment.start, _j = residueSegment.end; j < _j; j++) {
+                //     l.element = elements[j];
+                // }
+                // TODO get proper trace element
+                pos(l.element, curV)
+
+                if (seqId - 1 === prevSeqId) {
+                    // TODO draw trace
+                    builder.setId(residueSegment.start)
+                    builder.addCylinder(prevV, curV, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+                }
+
+                Vec3.copy(prevV, curV)
+                prevSeqId = seqId
+
+                if (i % 10000 === 0 && ctx.shouldUpdate) {
+                    await ctx.update({ message: 'Cartoon mesh', current: i, max: polymerElementCount });
+                }
+                ++i
+            }
+        }
+    } else if (Unit.isSpheres(unit)) {
+        let prevSeqIdEnd = -1
+        for (let i = 0, il = elements.length; i < il; ++i) {
+            l.element = elements[i]
+            if (StructureProperties.entity.type(l) !== 'polymer') continue;
+            // console.log(elementLabel(l), StructureProperties.entity.type(l))
+
+            pos(elements[i], curV)
+            const seqIdBegin = StructureProperties.coarse.seq_id_begin(l)
+            const seqIdEnd = StructureProperties.coarse.seq_id_end(l)
+
+            pos(elements[i], curV)
+
+            if (seqIdBegin - 1 === prevSeqIdEnd) {
+                // TODO draw trace
+                builder.setId(i)
+                builder.addCylinder(prevV, curV, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+            }
+
+            Vec3.copy(prevV, curV)
+            prevSeqIdEnd = seqIdEnd
+
+            if (i % 10000 === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'Backbone mesh', current: i, max: polymerElementCount });
+            }
+        }
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerTraceProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerTraceProps = Partial<typeof DefaultPolymerTraceProps>
+
+export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerTraceProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerTraceProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerTraceProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerTraceProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerTraceMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerTraceProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerTraceMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            const { objectId, instanceId, elementId } = pickingId
+            if (renderObject.id === objectId) {
+                const unit = currentGroup.units[instanceId]
+                const indices = OrderedSet.ofSingleton(elementId as Element.Index);
+                return Element.Loci([{ unit, indices }])
+            }
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 1 - 3
src/mol-geo/representation/structure/visual/util/common.ts

@@ -1,5 +1,3 @@
-
-
 /**
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
@@ -68,4 +66,4 @@ export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, pro
         case 'physical':
             return physicalSizeData(defaults(props.factor, 1), { group, vertexMap })
     }
-}
+}

+ 13 - 3
src/mol-geo/representation/structure/visual/util/element.ts

@@ -13,10 +13,11 @@ import { Mesh } from '../../../../shape/mesh';
 import { MeshBuilder } from '../../../../shape/mesh-builder';
 import { ValueCell, defaults } from 'mol-util';
 import { TextureImage } from 'mol-gl/renderable/util';
-import { Loci, isEveryLoci } from 'mol-model/loci';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction } from '../../../../util/marker-data';
-import { Interval } from 'mol-data/int';
+import { Interval, OrderedSet } from 'mol-data/int';
 import { getPhysicalRadius } from '../../../../theme/structure/size/physical';
+import { PickingId } from '../../../../util/picking';
 
 export function getElementRadius(unit: Unit, props: SizeTheme): Element.Property<number> {
     switch (props.name) {
@@ -55,7 +56,6 @@ export async function createElementSphereMesh(ctx: RuntimeContext, unit: Unit, r
     return meshBuilder.getMesh()
 }
 
-
 export function markElement(tMarker: ValueCell<TextureImage>, group: Unit.SymmetryGroup, loci: Loci, action: MarkerAction) {
     let changed = false
     const elementCount = group.elements.length
@@ -91,3 +91,13 @@ export function markElement(tMarker: ValueCell<TextureImage>, group: Unit.Symmet
         ValueCell.update(tMarker, tMarker.ref.value)
     }
 }
+
+export function getElementLoci(id: number, group: Unit.SymmetryGroup, pickingId: PickingId) {
+    const { objectId, instanceId, elementId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const indices = OrderedSet.ofSingleton(elementId as Element.Index);
+        return Element.Loci([{ unit, indices }])
+    }
+    return EmptyLoci
+}

+ 93 - 0
src/mol-geo/representation/structure/visual/util/polymer.ts

@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Element, StructureProperties } from 'mol-model/structure';
+import { Segmentation } from 'mol-data/int';
+// import Iterator from 'mol-data/iterator';
+
+export function getPolymerElementCount(unit: Unit) {
+    let count = 0
+    const { elements } = unit
+    const l = Element.Location(unit)
+    if (Unit.isAtomic(unit)) {
+        const { chainSegments, residueSegments } = unit.model.atomicHierarchy
+        const chainsIt = Segmentation.transientSegments(chainSegments, elements);
+        const residuesIt = Segmentation.transientSegments(residueSegments, elements);
+        while (chainsIt.hasNext) {
+            residuesIt.setSegment(chainsIt.move());
+            while (residuesIt.hasNext) {
+                l.element = elements[residuesIt.move().start];
+                if (StructureProperties.entity.type(l) === 'polymer') count++
+            }
+        }
+    } else if (Unit.isSpheres(unit)) {
+        for (let i = 0, il = elements.length; i < il; ++i) {
+            l.element = elements[i]
+            if (StructureProperties.entity.type(l) === 'polymer') count++
+        }
+    }
+    return count
+}
+
+// interface PolymerTrace {
+//     center: Element
+//     direction: Element
+// }
+
+// export class PolymerTraceIterator<T extends number = number> implements Iterator<PolymerTrace> {
+//     private segmentMin = 0;
+//     private segmentMax = 0;
+//     private setRange = Interval.Empty;
+//     private value: TraceSegment = { index: 0, start: 0 as T, end: 0 as T };
+
+//     hasNext: boolean = false;
+
+//     move() {
+//         while (this.hasNext) {
+//             if (this.updateValue()) {
+//                 this.value.index = this.segmentMin++;
+//                 this.hasNext = this.segmentMax >= this.segmentMin && Interval.size(this.setRange) > 0;
+//                 break;
+//             } else {
+//                 this.updateSegmentRange();
+//             }
+//         }
+//         return this.value;
+//     }
+
+//     private updateValue() {
+//         const segmentEnd = this.segments[this.segmentMin + 1];
+//         // TODO: add optimized version for interval and array?
+//         const setEnd = OrderedSet.findPredecessorIndexInInterval(this.set, segmentEnd, this.setRange);
+//         this.value.start = Interval.start(this.setRange) as T;
+//         this.value.end = setEnd as T;
+//         this.setRange = Interval.ofBounds(setEnd, Interval.end(this.setRange));
+//         return setEnd > this.value.start;
+//     }
+
+//     private updateSegmentRange() {
+//         const sMin = Interval.min(this.setRange), sMax = Interval.max(this.setRange);
+//         if (sMax < sMin) {
+//             this.hasNext = false;
+//             return;
+//         }
+
+//         this.segmentMin = this.segmentMap[OrderedSet.getAt(this.set, sMin)];
+//         this.segmentMax = this.segmentMap[OrderedSet.getAt(this.set, sMax)];
+
+//         this.hasNext = this.segmentMax >= this.segmentMin;
+//     }
+
+//     setSegment(segment: Segs.Segment<T>) {
+//         this.setRange = Interval.ofBounds(segment.start, segment.end);
+//         this.updateSegmentRange();
+//     }
+
+//     constructor(private segments: SortedArray, private segmentMap: Int32Array, private set: OrderedSet, inputRange: Interval) {
+//         this.setRange = inputRange;
+//         this.updateSegmentRange();
+//     }
+// }

+ 3 - 7
src/mol-geo/representation/volume/index.ts

@@ -27,7 +27,7 @@ export function VolumeRepresentation<P>(visualCtor: (volumeData: VolumeData) =>
             _volumeData = volumeData
             const visual = visualCtor(_volumeData)
             await visual.create(ctx, _volumeData, props)
-            renderObjects.push(...visual.renderObjects)
+            renderObjects.push(visual.renderObject)
         });
     }
 
@@ -36,12 +36,8 @@ export function VolumeRepresentation<P>(visualCtor: (volumeData: VolumeData) =>
     }
 
     return {
-        get renderObjects () {
-            return renderObjects
-        },
-        get props () {
-            return _props
-        },
+        get renderObjects () { return renderObjects },
+        get props () { return _props },
         create,
         update,
         getLoci(pickingId: PickingId) {

+ 4 - 7
src/mol-geo/representation/volume/surface.ts

@@ -10,7 +10,7 @@ import { Task, RuntimeContext } from 'mol-task'
 import { computeMarchingCubes } from '../../util/marching-cubes/algorithm';
 import { Mesh } from '../../shape/mesh';
 import { VolumeVisual } from '.';
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object';
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object';
 import { fillSerial } from 'mol-gl/renderable/util';
 import { ValueCell, defaults } from 'mol-util';
 import { Mat4 } from 'mol-math/linear-algebra';
@@ -51,14 +51,12 @@ export const DefaultSurfaceProps = {
 export type SurfaceProps = Partial<typeof DefaultSurfaceProps>
 
 export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
-    const renderObjects: RenderObject[] = []
-    let surface: MeshRenderObject
+    let renderObject: MeshRenderObject
     let curProps = DefaultSurfaceProps
 
     return {
-        renderObjects,
+        get renderObject () { return renderObject },
         async create(ctx: RuntimeContext, volume: VolumeData, props: SurfaceProps = {}) {
-            renderObjects.length = 0 // clear
             props = { ...DefaultSurfaceProps, ...props }
 
             const mesh = await computeVolumeSurface(volume, curProps.isoValue).runAsChild(ctx)
@@ -96,8 +94,7 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
                 visible: defaults(props.visible, true)
             }
 
-            surface = createMeshRenderObject(values, state)
-            renderObjects.push(surface)
+            renderObject = createMeshRenderObject(values, state)
         },
         async update(ctx: RuntimeContext, props: SurfaceProps) {
             // TODO

+ 1 - 1
src/mol-io/reader/cif/schema/mmcif.ts

@@ -68,7 +68,7 @@ export const mmCIF_Schema = {
         mon_nstd_flag: Aliased<'no' | 'n' | 'yes' | 'y'>(str),
         name: str,
         type: Aliased<'D-peptide linking' | 'L-peptide linking' | 'D-peptide NH3 amino terminus' | 'L-peptide NH3 amino terminus' | 'D-peptide COOH carboxy terminus' | 'L-peptide COOH carboxy terminus' | 'DNA linking' | 'RNA linking' | 'L-RNA linking' | 'L-DNA linking' | 'DNA OH 5 prime terminus' | 'RNA OH 5 prime terminus' | 'DNA OH 3 prime terminus' | 'RNA OH 3 prime terminus' | 'D-saccharide 1,4 and 1,4 linking' | 'L-saccharide 1,4 and 1,4 linking' | 'D-saccharide 1,4 and 1,6 linking' | 'L-saccharide 1,4 and 1,6 linking' | 'L-saccharide' | 'D-saccharide' | 'saccharide' | 'non-polymer' | 'peptide linking' | 'peptide-like' | 'L-gamma-peptide, C-delta linking' | 'D-gamma-peptide, C-delta linking' | 'L-beta-peptide, C-gamma linking' | 'D-beta-peptide, C-gamma linking' | 'other'>(str),
-        pdbx_synonyms: str,
+        pdbx_synonyms: List(';', x => x),
     },
     chem_comp_bond: {
         atom_id_1: str,

+ 1 - 0
src/mol-model/structure/export/mmcif.ts

@@ -37,6 +37,7 @@ const Categories = [
     copy_mmCif_category('exptl'),
     copy_mmCif_category('cell'),
     copy_mmCif_category('symmetry'),
+    copy_mmCif_category('chem_comp'),
     _entity,
     _atom_site
 ];

+ 28 - 2
src/mol-model/structure/model/formats/mmcif.ts

@@ -24,6 +24,8 @@ import { getSequence } from './mmcif/sequence';
 import { sortAtomSite } from './mmcif/sort';
 
 import mmCIF_Format = Format.mmCIF
+import { ChemicalComponent } from '../properties/chemical-component';
+import { ComponentType, getMoleculeType } from '../types';
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -98,6 +100,26 @@ function getAsymIdSerialMap(format: mmCIF_Format) {
     return map;
 }
 
+function getChemicalComponentMap(format: mmCIF_Format) {
+    const map = new Map<string, ChemicalComponent>();
+    const { id, type, name, pdbx_synonyms, formula, formula_weight } = format.data.chem_comp
+    for (let i = 0, il = id.rowCount; i < il; ++i) {
+        const _id = id.value(i)
+        const _type = type.value(i)
+        const cc: ChemicalComponent = {
+            id: _id,
+            type: ComponentType[_type],
+            moleculeType: getMoleculeType(_type, _id),
+            name: name.value(i),
+            synonyms: pdbx_synonyms.value(i),
+            formula: formula.value(i),
+            formulaWeight: formula_weight.value(i),
+        }
+        map.set(_id, cc)
+    }
+    return map
+}
+
 function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, previous?: Model): Model {
     const atomic = getAtomicHierarchyAndConformation(format, atom_site, entities, previous);
     if (previous && atomic.sameAsPrevious) {
@@ -111,6 +133,7 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
 
     const modifiedResidueNameMap = getModifiedResidueNameMap(format);
     const asymIdSerialMap = getAsymIdSerialMap(format)
+    const chemicalComponentMap = getChemicalComponentMap(format)
 
     return {
         id: UUID.create(),
@@ -127,7 +150,8 @@ function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities
         properties: {
             secondaryStructure: getSecondaryStructureMmCif(format.data, atomic.hierarchy),
             modifiedResidueNameMap,
-            asymIdSerialMap
+            asymIdSerialMap,
+            chemicalComponentMap
         },
         customProperties: new CustomProperties(),
         _staticPropertyData: Object.create(null),
@@ -140,6 +164,7 @@ function createModelIHM(format: mmCIF_Format, data: IHMData): Model {
     const coarse = getIHMCoarse(data);
     const modifiedResidueNameMap = getModifiedResidueNameMap(format);
     const asymIdSerialMap = getAsymIdSerialMap(format)
+    const chemicalComponentMap = getChemicalComponentMap(format)
 
     return {
         id: UUID.create(),
@@ -156,7 +181,8 @@ function createModelIHM(format: mmCIF_Format, data: IHMData): Model {
         properties: {
             secondaryStructure: getSecondaryStructureMmCif(format.data, atomic.hierarchy),
             modifiedResidueNameMap,
-            asymIdSerialMap
+            asymIdSerialMap,
+            chemicalComponentMap
         },
         customProperties: new CustomProperties(),
         _staticPropertyData: Object.create(null),

+ 8 - 3
src/mol-model/structure/model/formats/mmcif/atomic.ts

@@ -20,15 +20,18 @@ import mmCIF_Format = Format.mmCIF
 type AtomSite = mmCIF_Database['atom_site']
 
 function findHierarchyOffsets(atom_site: AtomSite) {
-    if (atom_site._rowCount === 0) return { residues: [], chains: [] };
+    if (atom_site._rowCount === 0) return { residues: [], polymers: [], chains: [] };
 
     const start = 0, end = atom_site._rowCount;
-    const residues = [start as Element], chains = [start as Element];
+    const residues = [start as Element], chains = [start as Element], polymers = [start as Element];
 
     const { label_entity_id, label_asym_id, label_seq_id, auth_seq_id, pdbx_PDB_ins_code, label_comp_id } = atom_site;
 
     for (let i = start + 1; i < end; i++) {
         const newChain = !label_entity_id.areValuesEqual(i - 1, i) || !label_asym_id.areValuesEqual(i - 1, i);
+        // TODO improve???
+        const newPolymer = newChain
+            || label_seq_id.value(i - 1) !== label_seq_id.value(i) - 1;
         const newResidue = newChain
             || !label_seq_id.areValuesEqual(i - 1, i)
             || !auth_seq_id.areValuesEqual(i - 1, i)
@@ -36,9 +39,10 @@ function findHierarchyOffsets(atom_site: AtomSite) {
             || !label_comp_id.areValuesEqual(i - 1, i);
 
         if (newResidue) residues[residues.length] = i as Element;
+        if (newPolymer) polymers[polymers.length] = i as Element;
         if (newChain) chains[chains.length] = i as Element;
     }
-    return { residues, chains };
+    return { residues, polymers, chains };
 }
 
 function createHierarchyData(atom_site: AtomSite, offsets: { residues: ArrayLike<number>, chains: ArrayLike<number> }): AtomicData {
@@ -92,6 +96,7 @@ export function getAtomicHierarchyAndConformation(format: mmCIF_Format, atom_sit
     const hierarchySegments: AtomicSegments = {
         residueSegments: Segmentation.ofOffsets(hierarchyOffsets.residues, Interval.ofBounds(0, atom_site._rowCount)),
         chainSegments: Segmentation.ofOffsets(hierarchyOffsets.chains, Interval.ofBounds(0, atom_site._rowCount)),
+        polymerSegments: Segmentation.ofOffsets(hierarchyOffsets.polymers, Interval.ofBounds(0, atom_site._rowCount)),
     }
 
     const hierarchyKeys = getAtomicKeys(hierarchyData, entities, hierarchySegments);

+ 13 - 5
src/mol-model/structure/model/formats/mmcif/ihm.ts

@@ -78,16 +78,24 @@ function getGaussianConformation(data: mmCIF['ihm_gaussian_obj_site']): CoarseGa
     };
 }
 
-function getChainSegments(asym_id: Column<string>) {
-    const offsets = [0 as Element];
+function getSegments(asym_id: Column<string>, seq_id_begin: Column<number>, seq_id_end: Column<number>) {
+    const polymerOffsets = [0 as Element], chainOffsets = [0 as Element];
     for (let i = 1, _i = asym_id.rowCount; i < _i; i++) {
-        if (!asym_id.areValuesEqual(i - 1, i)) offsets[offsets.length] = i as Element;
+        const newChain = !asym_id.areValuesEqual(i - 1, i);
+        const newPolymer = newChain
+            || seq_id_end.value(i - 1) !== seq_id_begin.value(i) - 1;
+
+        if (newPolymer) polymerOffsets[polymerOffsets.length] = i as Element;
+        if (newChain) chainOffsets[chainOffsets.length] = i as Element;
     }
 
-    return Segmentation.ofOffsets(offsets, Interval.ofBounds(0, asym_id.rowCount));
+    return {
+        chainSegments: Segmentation.ofOffsets(chainOffsets, Interval.ofBounds(0, asym_id.rowCount)),
+        polymerSegments: Segmentation.ofOffsets(polymerOffsets, Interval.ofBounds(0, asym_id.rowCount))
+    }
 }
 
 function getData(data: mmCIF['ihm_sphere_obj_site'] | mmCIF['ihm_gaussian_obj_site']): CoarseElementData {
     const { entity_id, seq_id_begin, seq_id_end, asym_id } = data;
-    return { count: entity_id.rowCount, entity_id, asym_id, seq_id_begin, seq_id_end, chainSegments: getChainSegments(asym_id) };
+    return { count: entity_id.rowCount, entity_id, asym_id, seq_id_begin, seq_id_end, ...getSegments(asym_id, seq_id_begin, seq_id_end) };
 }

+ 5 - 2
src/mol-model/structure/model/model.ts

@@ -14,8 +14,9 @@ import { Entities } from './properties/common';
 import { CustomProperties } from './properties/custom';
 import { SecondaryStructure } from './properties/seconday-structure';
 
-// import from_gro from './formats/gro'
 import from_mmCIF from './formats/mmcif'
+import { ChemicalComponent } from './properties/chemical-component';
+
 /**
  * Interface to the "source data" of the molecule.
  *
@@ -42,8 +43,10 @@ interface Model extends Readonly<{
         readonly secondaryStructure: SecondaryStructure,
         /** maps modified residue name to its parent */
         readonly modifiedResidueNameMap: Map<string, string>,
-        /** maps asym ids to serial numbers that are unique per asym id */
+        /** maps asym id to unique serial number */
         readonly asymIdSerialMap: Map<string, number>
+        /** maps residue name to `ChemicalComponent` data */
+        readonly chemicalComponentMap: Map<string, ChemicalComponent>
     },
 
     customProperties: CustomProperties,

+ 6 - 1
src/mol-model/structure/model/properties/atomic/hierarchy.ts

@@ -49,7 +49,12 @@ export interface AtomicData {
 
 export interface AtomicSegments {
     residueSegments: Segmentation<Element>,
-    chainSegments: Segmentation<Element>
+    chainSegments: Segmentation<Element>,
+    /**
+     * bonded/connected stretches of polymer chains, i.e. a chain will be
+     * broken into multiple polymer segments if there are missing residues
+     */
+    polymerSegments: Segmentation<Element>
     // TODO: include entity segments?
 }
 

+ 17 - 0
src/mol-model/structure/model/properties/chemical-component.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 { MoleculeType, ComponentType } from '../types'
+
+export interface ChemicalComponent {
+    id: string
+    type: ComponentType
+    moleculeType: MoleculeType
+    name: string
+    synonyms: string[]
+    formula: string
+    formulaWeight: number
+}

+ 6 - 1
src/mol-model/structure/model/properties/coarse/hierarchy.ts

@@ -27,7 +27,12 @@ export interface CoarseElementData {
     seq_id_begin: Column<number>,
     seq_id_end: Column<number>,
 
-    chainSegments: Segmentation<Element>
+    chainSegments: Segmentation<Element>,
+    /**
+     * bonded/connected stretches of polymer chains, i.e. a chain will be
+     * broken into multiple polymer segments if there are missing residues
+     */
+    polymerSegments: Segmentation<Element>
 }
 
 export type CoarseElements = CoarsedElementKeys & CoarseElementData

+ 120 - 46
src/mol-model/structure/model/types.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 Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -30,63 +30,137 @@ export function ElementSymbol(s: string): ElementSymbol {
     return _esCache[s] || s.toUpperCase();
 }
 
+/** Entity types as defined in the mmCIF dictionary */
 export const enum EntityType {
-    Unknown = 'unknown',
-    Polymer = 'polymer',
-    NonPolymer = 'non-polymer',
-    Macrolide = 'macrolide',
-    Water = 'water'
+    'unknown', 'polymer', 'non-polymer', 'macrolide', 'water'
 }
 
 export const enum MoleculeType {
-    Unknown,
-    Water,
-    Ion,
-    Protein,
+    /** the molecule type is not known */
+    unknown,
+    /** a known, but here not listed molecule type */
+    other,
+    /** water molecule */
+    water,
+    /** small ionic molecule */
+    ion,
+    /** protein, e.g. component type included in `ProteinComponentTypeNames` */
+    protein,
+    /** RNA, e.g. component type included in `RNAComponentTypeNames` */
     RNA,
+    /** DNA, e.g. component type included in `DNAComponentTypeNames` */
     DNA,
-    Saccharide
+    /** sacharide, e.g. component type included in `SaccharideComponentTypeNames` */
+    saccharide
 }
 
-export const enum BackboneType {
-    Unknown,
-    Protein,
-    RNA,
-    DNA,
-    CgProtein,
-    CgRNA,
-    CgDNA
+/** Chemical component types as defined in the mmCIF CCD */
+export enum ComponentType {
+    // protein
+    'D-peptide linking', 'L-peptide linking', 'D-peptide NH3 amino terminus',
+    'L-peptide NH3 amino terminus', 'D-peptide COOH carboxy terminus',
+    'L-peptide COOH carboxy terminus', 'peptide linking', 'peptide-like',
+    'L-gamma-peptide, C-delta linking', 'D-gamma-peptide, C-delta linking',
+    'L-beta-peptide, C-gamma linking', 'D-beta-peptide, C-gamma linking',
+
+    // DNA
+    'DNA linking', 'L-DNA linking', 'DNA OH 5 prime terminus', 'DNA OH 3 prime terminus',
+
+    // RNA
+    'RNA linking', 'L-RNA linking', 'RNA OH 5 prime terminus', 'RNA OH 3 prime terminus',
+
+    // sacharide
+    'D-saccharide 1,4 and 1,4 linking', 'L-saccharide 1,4 and 1,4 linking',
+    'D-saccharide 1,4 and 1,6 linking', 'L-saccharide 1,4 and 1,6 linking', 'L-saccharide',
+    'D-saccharide', 'saccharide',
+
+    'non-polymer', 'other'
 }
 
-const _chemCompNonPolymer = ['NON-POLYMER'];
-const _chemCompOther = ['OTHER'];
-const _chemCompSaccharide = [
-    'D-SACCHARIDE', 'D-SACCHARIDE 1,4 AND 1,4 LINKING', 'D-SACCHARIDE 1,4 AND 1,6 LINKING',
-    'L-SACCHARIDE', 'L-SACCHARIDE 1,4 AND 1,4 LINKING', 'L-SACCHARIDE 1,4 AND 1,6 LINKING',
-    'SACCHARIDE'
-];
-
-export const ChemComp = {
-    Protein: [
-        'D-BETA-PEPTIDE, C-GAMMA LINKING', 'D-GAMMA-PEPTIDE, C-DELTA LINKING',
-        'D-PEPTIDE COOH CARBOXY TERMINUS', 'D-PEPTIDE NH3 AMINO TERMINUS', 'D-PEPTIDE LINKING',
-        'L-BETA-PEPTIDE, C-GAMMA LINKING', 'L-GAMMA-PEPTIDE, C-DELTA LINKING',
-        'L-PEPTIDE COOH CARBOXY TERMINUS', 'L-PEPTIDE NH3 AMINO TERMINUS', 'L-PEPTIDE LINKING',
-        'PEPTIDE LINKING', 'PEPTIDE-LIKE'
-    ],
-    RNA: [
-        'RNA OH 3 PRIME TERMINUS', 'RNA OH 5 PRIME TERMINUS', 'RNA LINKING'
-    ],
-    DNA: [
-        'DNA OH 3 PRIME TERMINUS', 'DNA OH 5 PRIME TERMINUS', 'DNA LINKING',
-        'L-DNA LINKING', 'L-RNA LINKING'
-    ],
-    Saccharide: _chemCompSaccharide,
-    Other: _chemCompOther,
-    NonPolymer: _chemCompNonPolymer,
-    Hetero: _chemCompNonPolymer.concat(_chemCompOther, _chemCompSaccharide)
+/** Chemical component type names for protein */
+export const ProteinComponentTypeNames = [
+    'D-peptide linking', 'L-peptide linking', 'D-peptide NH3 amino terminus',
+    'L-peptide NH3 amino terminus', 'D-peptide COOH carboxy terminus',
+    'L-peptide COOH carboxy terminus', 'peptide linking', 'peptide-like',
+    'L-gamma-peptide, C-delta linking', 'D-gamma-peptide, C-delta linking',
+    'L-beta-peptide, C-gamma linking', 'D-beta-peptide, C-gamma linking',
+]
+
+/** Chemical component type names for DNA */
+export const DNAComponentTypeNames = [
+    'DNA linking', 'L-DNA linking', 'DNA OH 5 prime terminus', 'DNA OH 3 prime terminus',
+]
+
+/** Chemical component type names for RNA */
+export const RNAComponentTypeNames = [
+    'RNA linking', 'L-RNA linking', 'RNA OH 5 prime terminus', 'RNA OH 3 prime terminus',
+]
+
+/** Chemical component type names for saccharide */
+export const SaccharideComponentTypeNames = [
+    'D-saccharide 1,4 and 1,4 linking', 'L-saccharide 1,4 and 1,4 linking',
+    'D-saccharide 1,4 and 1,6 linking', 'L-saccharide 1,4 and 1,6 linking', 'L-saccharide',
+    'D-saccharide', 'saccharide',
+]
+
+/** Common names for water molecules */
+export const WaterNames = [
+    'SOL', 'WAT', 'HOH', 'H2O', 'W', 'DOD', 'D3O', 'TIP3', 'TIP4', 'SPC'
+]
+
+/** get the molecule type from component type and id */
+export function getMoleculeType(compType: string, compId: string) {
+    if (ProteinComponentTypeNames.includes(compType)) {
+        return MoleculeType.protein
+    } else if (RNAComponentTypeNames.includes(compType)) {
+        return MoleculeType.RNA
+    } else if (DNAComponentTypeNames.includes(compType)) {
+        return MoleculeType.DNA
+    } else if (SaccharideComponentTypeNames.includes(compType)) {
+        return MoleculeType.saccharide
+    } else if (WaterNames.includes(compId)) {
+        return MoleculeType.water
+    } else if (IonNames.includes(compId)) {
+        return MoleculeType.ion
+    } else {
+        return MoleculeType.unknown
+    }
 }
 
+/**
+ * all chemical components with the word "ion" in their name, Sep 2016
+ *
+ * SET SESSION group_concat_max_len = 1000000;
+ * SELECT GROUP_CONCAT(id_ ORDER BY id_ ASC SEPARATOR '", "') from
+ * (
+ *     SELECT count(obj_id) as c, id_
+ *     FROM pdb.chem_comp WHERE name LIKE "% ION%"
+ *     GROUP BY id_
+ * ) AS t1;
+ */
+export const IonNames = [
+  '118', '119', '1AL', '1CU', '2FK', '2HP', '2OF', '3CO',
+  '3MT', '3NI', '3OF', '3P8', '4MO', '4PU', '543', '6MO', 'ACT', 'AG', 'AL',
+  'ALF', 'AM', 'ATH', 'AU', 'AU3', 'AUC', 'AZI', 'BA', 'BCT', 'BEF', 'BF4', 'BO4',
+  'BR', 'BS3', 'BSY', 'CA', 'CAC', 'CD', 'CD1', 'CD3', 'CD5', 'CE', 'CHT', 'CL',
+  'CO', 'CO3', 'CO5', 'CON', 'CR', 'CS', 'CSB', 'CU', 'CU1', 'CU3', 'CUA', 'CUZ',
+  'CYN', 'DME', 'DMI', 'DSC', 'DTI', 'DY', 'E4N', 'EDR', 'EMC', 'ER3', 'EU',
+  'EU3', 'F', 'FE', 'FE2', 'FPO', 'GA', 'GD3', 'GEP', 'HAI', 'HG', 'HGC', 'IN',
+  'IOD', 'IR', 'IR3', 'IRI', 'IUM', 'K', 'KO4', 'LA', 'LCO', 'LCP', 'LI', 'LU',
+  'MAC', 'MG', 'MH2', 'MH3', 'MLI', 'MLT', 'MMC', 'MN', 'MN3', 'MN5', 'MN6',
+  'MO1', 'MO2', 'MO3', 'MO4', 'MO5', 'MO6', 'MOO', 'MOS', 'MOW', 'MW1', 'MW2',
+  'MW3', 'NA', 'NA2', 'NA5', 'NA6', 'NAO', 'NAW', 'NCO', 'NET', 'NH4', 'NI',
+  'NI1', 'NI2', 'NI3', 'NO2', 'NO3', 'NRU', 'O4M', 'OAA', 'OC1', 'OC2', 'OC3',
+  'OC4', 'OC5', 'OC6', 'OC7', 'OC8', 'OCL', 'OCM', 'OCN', 'OCO', 'OF1', 'OF2',
+  'OF3', 'OH', 'OS', 'OS4', 'OXL', 'PB', 'PBM', 'PD', 'PDV', 'PER', 'PI', 'PO3',
+  'PO4', 'PR', 'PT', 'PT4', 'PTN', 'RB', 'RH3', 'RHD', 'RU', 'SB', 'SCN', 'SE4',
+  'SEK', 'SM', 'SMO', 'SO3', 'SO4', 'SR', 'T1A', 'TB', 'TBA', 'TCN', 'TEA', 'TH',
+  'THE', 'TL', 'TMA', 'TRA', 'UNX', 'V', 'VN3', 'VO4', 'W', 'WO5', 'Y1', 'YB',
+  'YB2', 'YH', 'YT3', 'ZCM', 'ZN', 'ZN2', 'ZN3', 'ZNO', 'ZO3',
+    // additional ion names
+  'OHX'
+]
+
 export interface SecondaryStructureType extends BitFlags<SecondaryStructureType.Flag> { }
 export namespace SecondaryStructureType {
     export const Helix = ['h', 'g', 'i']

+ 34 - 6
src/mol-view/stage.ts

@@ -7,11 +7,14 @@
 import Viewer from 'mol-view/viewer'
 import { StateContext } from './state/context';
 import { Progress } from 'mol-task';
-import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick, StructureToDistanceRestraint } from './state/transform';
+import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick, StructureToDistanceRestraint, StructureToCartoon, StructureToBackbone } from './state/transform';
 import { UrlEntity } from './state/entity';
 import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
 import { Context } from 'mol-app/context/context';
 import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-stick';
+import { CartoonProps } from 'mol-geo/representation/structure/cartoon';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/backbone';
 
 const spacefillProps: SpacefillProps = {
     doubleSided: true,
@@ -28,6 +31,28 @@ const ballAndStickProps: BallAndStickProps = {
     useFog: false
 }
 
+const distanceRestraintProps: DistanceRestraintProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    linkRadius: 0.5,
+    quality: 'auto',
+    useFog: false
+}
+
+const backboneProps: BackboneProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
+const cartoonProps: CartoonProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
 export class Stage {
     viewer: Viewer
     ctx = new StateContext(Progress.format)
@@ -43,14 +68,15 @@ export class Stage {
 
         // this.loadPdbid('1jj2')
         // this.loadPdbid('4umt') // ligand has bond with order 3
-        this.loadPdbid('1crn') // small
+        // this.loadPdbid('1crn') // small
         // this.loadPdbid('1rb8') // virus
         // this.loadPdbid('1blu') // metal coordination
         // this.loadPdbid('3pqr') // inter unit bonds
         // this.loadPdbid('4v5a') // ribosome
+        // this.loadPdbid('3j3q') // ...
         // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
 
-        // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`)
+        this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`)
     }
 
     async loadMmcifUrl (url: string) {
@@ -58,9 +84,11 @@ export class Stage {
         const modelEntity = await MmcifUrlToModel.apply(this.ctx, urlEntity)
         const structureEntity = await ModelToStructure.apply(this.ctx, modelEntity)
 
-        StructureToSpacefill.apply(this.ctx, structureEntity, { ...spacefillProps, visible: true })
-        StructureToBallAndStick.apply(this.ctx, structureEntity, ballAndStickProps)
-        StructureToDistanceRestraint.apply(this.ctx, structureEntity, ballAndStickProps)
+        StructureToSpacefill.apply(this.ctx, structureEntity, { ...spacefillProps, visible: false })
+        StructureToBallAndStick.apply(this.ctx, structureEntity, { ...ballAndStickProps, visible: false })
+        StructureToDistanceRestraint.apply(this.ctx, structureEntity, { ...distanceRestraintProps, visible: false })
+        StructureToBackbone.apply(this.ctx, structureEntity, { ...backboneProps, visible: true })
+        StructureToCartoon.apply(this.ctx, structureEntity, { ...cartoonProps, visible: false })
 
         this.globalContext.components.sequenceView.setState({ structure: structureEntity.value });
     }

+ 16 - 0
src/mol-view/state/entity.ts

@@ -15,6 +15,8 @@ import { StructureRepresentation } from 'mol-geo/representation/structure';
 import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
 import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-stick';
 import { DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint';
+import { CartoonProps } from 'mol-geo/representation/structure/cartoon';
+import { BackboneProps } from 'mol-geo/representation/structure/backbone';
 
 const getNextId = idFactory(1)
 
@@ -135,4 +137,18 @@ export namespace DistanceRestraintEntity {
     export function ofRepr(ctx: StateContext, repr: StructureRepresentation<DistanceRestraintProps>): DistanceRestraintEntity {
         return StateEntity.create(ctx, 'distancerestraint', repr )
     }
+}
+
+export type BackboneEntity = StateEntity<StructureRepresentation<BackboneProps>, 'backbone'>
+export namespace BackboneEntity {
+    export function ofRepr(ctx: StateContext, repr: StructureRepresentation<BackboneProps>): BackboneEntity {
+        return StateEntity.create(ctx, 'backbone', repr )
+    }
+}
+
+export type CartoonEntity = StateEntity<StructureRepresentation<CartoonProps>, 'cartoon'>
+export namespace CartoonEntity {
+    export function ofRepr(ctx: StateContext, repr: StructureRepresentation<CartoonProps>): CartoonEntity {
+        return StateEntity.create(ctx, 'cartoon', repr )
+    }
 }

+ 46 - 1
src/mol-view/state/transform.ts

@@ -5,7 +5,7 @@
  */
 
 import CIF from 'mol-io/reader/cif'
-import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity, DistanceRestraintEntity } from './entity';
+import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity, DistanceRestraintEntity, CartoonEntity, BackboneEntity } from './entity';
 import { Model, Structure, Format } from 'mol-model/structure';
 
 import { StateContext } from './context';
@@ -13,6 +13,8 @@ import StructureSymmetry from 'mol-model/structure/structure/symmetry';
 import { SpacefillProps, SpacefillRepresentation } from 'mol-geo/representation/structure/spacefill';
 import { BallAndStickProps, BallAndStickRepresentation } from 'mol-geo/representation/structure/ball-and-stick';
 import { DistanceRestraintRepresentation, DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint';
+import { CartoonRepresentation, CartoonProps } from 'mol-geo/representation/structure/cartoon';
+import { BackboneProps, BackboneRepresentation } from 'mol-geo/representation/structure/backbone';
 
 type transformer<I extends AnyEntity, O extends AnyEntity, P extends {}> = (ctx: StateContext, inputEntity: I, props?: P) => Promise<O>
 
@@ -124,6 +126,27 @@ export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateT
         console.log('stats', ctx.viewer.stats)
         return DistanceRestraintEntity.ofRepr(ctx, distanceRestraintRepr)
     })
+export type StructureToBackbone = StateTransform<StructureEntity, BackboneEntity, BackboneProps>
+export const StructureToBackbone: StructureToBackbone = StateTransform.create('structure', 'backbone', 'structure-to-backbone',
+            async function (ctx: StateContext, structureEntity: StructureEntity, props: BackboneProps = {}) {
+                const backboneRepr = BackboneRepresentation()
+                await backboneRepr.create(structureEntity.value, props).run(ctx.log)
+                ctx.viewer.add(backboneRepr)
+                ctx.viewer.requestDraw()
+                console.log('stats', ctx.viewer.stats)
+                return BackboneEntity.ofRepr(ctx, backboneRepr)
+            })
+
+export type StructureToCartoon = StateTransform<StructureEntity, CartoonEntity, CartoonProps>
+export const StructureToCartoon: StructureToCartoon = StateTransform.create('structure', 'cartoon', 'structure-to-cartoon',
+        async function (ctx: StateContext, structureEntity: StructureEntity, props: CartoonProps = {}) {
+            const cartoonRepr = CartoonRepresentation()
+            await cartoonRepr.create(structureEntity.value, props).run(ctx.log)
+            ctx.viewer.add(cartoonRepr)
+            ctx.viewer.requestDraw()
+            console.log('stats', ctx.viewer.stats)
+            return CartoonEntity.ofRepr(ctx, cartoonRepr)
+        })
 
 export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, SpacefillProps>
 export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update',
@@ -158,6 +181,28 @@ export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.c
         return NullEntity
     })
 
+export type BackboneUpdate = StateTransform<BackboneEntity, NullEntity, BackboneProps>
+export const BackboneUpdate: BackboneUpdate = StateTransform.create('backbone', 'null', 'backbone-update',
+            async function (ctx: StateContext, backboneEntity: BackboneEntity, props: BackboneProps = {}) {
+                const backboneRepr = backboneEntity.value
+                await backboneRepr.update(props).run(ctx.log)
+                ctx.viewer.add(backboneRepr)
+                ctx.viewer.requestDraw()
+                console.log('stats', ctx.viewer.stats)
+                return NullEntity
+            })
+
+export type CartoonUpdate = StateTransform<CartoonEntity, NullEntity, CartoonProps>
+export const CartoonUpdate: CartoonUpdate = StateTransform.create('cartoon', 'null', 'cartoon-update',
+        async function (ctx: StateContext, cartoonEntity: CartoonEntity, props: CartoonProps = {}) {
+            const cartoonRepr = cartoonEntity.value
+            await cartoonRepr.update(props).run(ctx.log)
+            ctx.viewer.add(cartoonRepr)
+            ctx.viewer.requestDraw()
+            console.log('stats', ctx.viewer.stats)
+            return NullEntity
+        })
+
 // composed
 
 export type MmcifUrlToModel = StateTransform<UrlEntity, ModelEntity, {}>