Browse Source

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

David Sehnal 6 years ago
parent
commit
5a70862808
88 changed files with 3212 additions and 2716 deletions
  1. 222 222
      package-lock.json
  2. 7 7
      package.json
  3. 4 1
      src/mol-app/ui/entity/tree.tsx
  4. 31 30
      src/mol-app/ui/transform/backbone.tsx
  5. 28 28
      src/mol-app/ui/transform/ball-and-stick.tsx
  6. 250 0
      src/mol-app/ui/transform/carbohydrate.tsx
  7. 65 30
      src/mol-app/ui/transform/cartoon.tsx
  8. 13 28
      src/mol-app/ui/transform/distance-restraint.tsx
  9. 8 8
      src/mol-app/ui/transform/file-loader.tsx
  10. 3 0
      src/mol-app/ui/transform/list.tsx
  11. 28 29
      src/mol-app/ui/transform/spacefill.tsx
  12. 10 10
      src/mol-app/ui/transform/url-loader.tsx
  13. 45 62
      src/mol-geo/primitive/box.ts
  14. 9 2
      src/mol-geo/primitive/octahedron.ts
  15. 12 47
      src/mol-geo/primitive/prism.ts
  16. 28 1
      src/mol-geo/primitive/pyramid.ts
  17. 6 37
      src/mol-geo/primitive/sheet.ts
  18. 13 13
      src/mol-geo/primitive/tube.ts
  19. 37 111
      src/mol-geo/primitive/wedge.ts
  20. 4 4
      src/mol-geo/representation/index.ts
  21. 0 88
      src/mol-geo/representation/structure/cartoon.ts
  22. 79 0
      src/mol-geo/representation/structure/complex-representation.ts
  23. 122 0
      src/mol-geo/representation/structure/complex-visual.ts
  24. 30 177
      src/mol-geo/representation/structure/index.ts
  25. 13 12
      src/mol-geo/representation/structure/representation/backbone.ts
  26. 23 22
      src/mol-geo/representation/structure/representation/ball-and-stick.ts
  27. 63 0
      src/mol-geo/representation/structure/representation/carbohydrate.ts
  28. 82 0
      src/mol-geo/representation/structure/representation/cartoon.ts
  29. 15 15
      src/mol-geo/representation/structure/representation/distance-restraint.ts
  30. 5 5
      src/mol-geo/representation/structure/representation/spacefill.ts
  31. 129 0
      src/mol-geo/representation/structure/units-representation.ts
  32. 127 0
      src/mol-geo/representation/structure/units-visual.ts
  33. 138 0
      src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts
  34. 97 124
      src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
  35. 58 102
      src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
  36. 10 14
      src/mol-geo/representation/structure/visual/element-point.ts
  37. 17 103
      src/mol-geo/representation/structure/visual/element-sphere.ts
  38. 44 105
      src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
  39. 39 104
      src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
  40. 16 102
      src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
  41. 43 110
      src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
  42. 39 123
      src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
  43. 47 110
      src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
  44. 50 119
      src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
  45. 73 37
      src/mol-geo/representation/structure/visual/util/common.ts
  46. 20 39
      src/mol-geo/representation/structure/visual/util/element.ts
  47. 20 23
      src/mol-geo/representation/structure/visual/util/link.ts
  48. 141 0
      src/mol-geo/representation/structure/visual/util/location-iterator.ts
  49. 2 2
      src/mol-geo/representation/util.ts
  50. 10 4
      src/mol-geo/representation/volume/index.ts
  51. 2 2
      src/mol-geo/representation/volume/surface.ts
  52. 20 3
      src/mol-geo/shape/mesh-builder.ts
  53. 0 49
      src/mol-geo/theme/index.ts
  54. 0 71
      src/mol-geo/theme/structure/color/chain-id.ts
  55. 0 22
      src/mol-geo/theme/structure/color/element-index.ts
  56. 0 17
      src/mol-geo/theme/structure/color/index.ts
  57. 0 21
      src/mol-geo/theme/structure/color/instance-index.ts
  58. 0 15
      src/mol-geo/theme/structure/size/index.ts
  59. 0 39
      src/mol-geo/theme/structure/size/physical.ts
  60. 28 74
      src/mol-geo/util/color-data.ts
  61. 1 1
      src/mol-geo/util/marker-data.ts
  62. 82 27
      src/mol-geo/util/size-data.ts
  63. 5 5
      src/mol-gl/_spec/renderer.spec.ts
  64. 3 1
      src/mol-gl/renderable/point.ts
  65. 6 0
      src/mol-gl/shader/point.vert
  66. 16 6
      src/mol-math/linear-algebra/3d/mat4.ts
  67. 17 0
      src/mol-model/location.ts
  68. 1 1
      src/mol-model/loci.ts
  69. 77 24
      src/mol-model/structure/structure/carbohydrates/compute.ts
  70. 1 2
      src/mol-model/structure/structure/carbohydrates/constants.ts
  71. 15 5
      src/mol-model/structure/structure/carbohydrates/data.ts
  72. 8 1
      src/mol-model/structure/structure/element.ts
  73. 16 6
      src/mol-model/structure/structure/unit/links.ts
  74. 6 5
      src/mol-model/structure/structure/unit/pair-restraints/data.ts
  75. 7 6
      src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts
  76. 30 18
      src/mol-view/stage.ts
  77. 18 10
      src/mol-view/state/entity.ts
  78. 87 63
      src/mol-view/state/transform.ts
  79. 50 0
      src/mol-view/theme/color.ts
  80. 52 0
      src/mol-view/theme/color/carbohydrate-symbol.ts
  81. 47 0
      src/mol-view/theme/color/chain-id.ts
  82. 50 0
      src/mol-view/theme/color/element-index.ts
  83. 23 12
      src/mol-view/theme/color/element-symbol.ts
  84. 18 0
      src/mol-view/theme/color/uniform.ts
  85. 40 0
      src/mol-view/theme/color/unit-index.ts
  86. 37 0
      src/mol-view/theme/size.ts
  87. 53 0
      src/mol-view/theme/size/physical.ts
  88. 21 0
      src/mol-view/theme/size/uniform.ts

File diff suppressed because it is too large
+ 222 - 222
package-lock.json


+ 7 - 7
package.json

@@ -74,9 +74,9 @@
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.0",
     "@types/jest": "^23.3.1",
-    "@types/node": "^10.5.7",
+    "@types/node": "^10.7.0",
     "@types/node-fetch": "^2.1.2",
-    "@types/react": "^16.4.8",
+    "@types/react": "^16.4.10",
     "@types/react-dom": "^16.0.7",
     "benchmark": "^2.1.4",
     "cpx": "^1.5.0",
@@ -88,18 +88,18 @@
     "graphql-code-generator": "^0.10.7",
     "graphql-codegen-typescript-template": "^0.10.7",
     "graphql-tag": "^2.9.2",
-    "jest": "^23.4.2",
+    "jest": "^23.5.0",
     "jest-raw-loader": "^1.0.1",
     "mini-css-extract-plugin": "^0.4.1",
-    "node-sass": "^4.9.2",
+    "node-sass": "^4.9.3",
     "raw-loader": "^0.5.1",
     "resolve-url-loader": "^2.3.0",
     "sass-loader": "^7.1.0",
-    "style-loader": "^0.22.0",
+    "style-loader": "^0.22.1",
     "ts-jest": "^23.1.3",
     "tslint": "^5.11.0",
     "typescript": "^3.0.1",
-    "uglify-js": "^3.4.6",
+    "uglify-js": "^3.4.7",
     "util.promisify": "^1.0.0",
     "webpack": "^4.16.5",
     "webpack-cli": "^3.1.0"
@@ -109,7 +109,7 @@
     "compression": "^1.7.3",
     "express": "^4.16.3",
     "graphql": "^0.13.2",
-    "graphql-request": "^1.8.1",
+    "graphql-request": "^1.8.2",
     "immutable": "^4.0.0-rc.9",
     "node-fetch": "^2.2.0",
     "react": "^16.4.2",

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

+ 31 - 30
src/mol-app/ui/transform/backbone.tsx

@@ -15,35 +15,28 @@ 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 { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
 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
+    colorTheme: ColorThemeProps
     colorValue: Color
-    sizeTheme: SizeTheme
+    sizeTheme: SizeThemeProps
     visible: boolean
     alpha: number
     depthMask: boolean
     useFog: boolean
     quality: VisualQuality
     unitKinds: Unit.Kind[]
+    radialSegments: number
 }
 
 export class Backbone extends View<Controller<any>, BackboneState, { transform: BackboneUpdate, entity: BackboneEntity, ctx: StateContext }> {
@@ -52,15 +45,16 @@ export class Backbone extends View<Controller<any>, BackboneState, { transform:
         flipSided: false,
         flatShaded: false,
         detail: 2,
-        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
         colorValue: 0x000000,
-        sizeTheme: { name: 'uniform' } as SizeTheme,
+        sizeTheme: { name: 'uniform', factor: 1 } as SizeThemeProps,
         visible: true,
         alpha: 1,
         depthMask: true,
         useFog: true,
         quality: 'auto' as VisualQuality,
-        unitKinds: [] as Unit.Kind[]
+        unitKinds: [] as Unit.Kind[],
+        radialSegments: 16
     }
 
     componentWillMount() {
@@ -68,7 +62,6 @@ export class Backbone extends View<Controller<any>, BackboneState, { transform:
     }
 
     update(state?: Partial<BackboneState>) {
-        console.log(state)
         const { transform, entity, ctx } = this.props
         const newState = { ...this.state, ...state }
         this.setState(newState)
@@ -86,7 +79,7 @@ export class Backbone extends View<Controller<any>, BackboneState, { transform:
             return <option key={value} value={value}>{value.toString()}</option>
         })
 
-        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+        const colorThemeOptions = ColorThemeNames.map((name, idx) => {
             return <option key={name} value={name}>{name}</option>
         })
 
@@ -137,19 +130,12 @@ export class Backbone extends View<Controller<any>, BackboneState, { transform:
                                     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 }
-                                            })
-                                        }
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
                                     }}
                                 >
                                     {colorThemeOptions}
@@ -235,6 +221,21 @@ export class Backbone extends View<Controller<any>, BackboneState, { transform:
                                 />
                             </div>
                         </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.factor || 1}
+                                    label='Size factor'
+                                    min={0.1}
+                                    max={3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, factor: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>

+ 28 - 28
src/mol-app/ui/transform/ball-and-stick.tsx

@@ -15,28 +15,20 @@ 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 { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
 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 BallAndStickState {
     doubleSided: boolean
     flipSided: boolean
     flatShaded: boolean
-    colorTheme: ColorTheme
+    colorTheme: ColorThemeProps
     colorValue: Color
-    sizeTheme: SizeTheme
+    sizeTheme: SizeThemeProps
     visible: boolean
     alpha: number
     depthMask: boolean
@@ -55,9 +47,9 @@ export class BallAndStick extends View<Controller<any>, BallAndStickState, { tra
         doubleSided: true,
         flipSided: false,
         flatShaded: false,
-        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
         colorValue: 0x000000,
-        sizeTheme: { name: 'uniform' } as SizeTheme,
+        sizeTheme: { name: 'uniform', value: 0.15 } as SizeThemeProps,
         visible: true,
         alpha: 1,
         depthMask: true,
@@ -89,7 +81,7 @@ export class BallAndStick extends View<Controller<any>, BallAndStickState, { tra
             return <option key={name} value={name}>{name}</option>
         })
 
-        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+        const colorThemeOptions = ColorThemeNames.map((name, idx) => {
             return <option key={name} value={name}>{name}</option>
         })
 
@@ -128,19 +120,12 @@ export class BallAndStick extends View<Controller<any>, BallAndStickState, { tra
                                     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 }
-                                            })
-                                        }
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
                                     }}
                                 >
                                     {colorThemeOptions}
@@ -226,6 +211,21 @@ export class BallAndStick extends View<Controller<any>, BallAndStickState, { tra
                                 />
                             </div>
                         </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.factor || 1}
+                                    label='Size factor'
+                                    min={0.1}
+                                    max={3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, factor: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>

+ 250 - 0
src/mol-app/ui/transform/carbohydrate.tsx

@@ -0,0 +1,250 @@
+/**
+ * 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 { CarbohydrateEntity } from 'mol-view/state/entity';
+import { CarbohydrateUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
+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';
+
+interface CarbohydrateState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    detail: number
+    colorTheme: ColorThemeProps
+    colorValue: Color
+    sizeTheme: SizeThemeProps
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
+    linkScale: number
+    linkSpacing: number
+    linkRadius: number
+    radialSegments: number
+}
+
+export class Carbohydrate extends View<Controller<any>, CarbohydrateState, { transform: CarbohydrateUpdate, entity: CarbohydrateEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform', factor: 1 } as SizeThemeProps,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[],
+        linkScale: 0.4,
+        linkSpacing: 1,
+        linkRadius: 0.25,
+        radialSegments: 16
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<CarbohydrateState>) {
+        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 = ColorThemeNames.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) => {
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
+                                    }}
+                                >
+                                    {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 className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.factor || 1}
+                                    label='Size factor'
+                                    min={0.1}
+                                    max={3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, factor: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 65 - 30
src/mol-app/ui/transform/cartoon.tsx

@@ -15,35 +15,31 @@ 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 { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
 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
+    colorTheme: ColorThemeProps
     colorValue: Color
-    sizeTheme: SizeTheme
+    sizeTheme: SizeThemeProps
     visible: boolean
     alpha: number
     depthMask: boolean
     useFog: boolean
     quality: VisualQuality
     unitKinds: Unit.Kind[]
+    linearSegments: number
+    radialSegments: number
+    aspectRatio: number
+    arrowFactor: number
 }
 
 export class Cartoon extends View<Controller<any>, CartoonState, { transform: CartoonUpdate, entity: CartoonEntity, ctx: StateContext }> {
@@ -52,15 +48,19 @@ export class Cartoon extends View<Controller<any>, CartoonState, { transform: Ca
         flipSided: false,
         flatShaded: false,
         detail: 2,
-        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
         colorValue: 0x000000,
-        sizeTheme: { name: 'uniform' } as SizeTheme,
+        sizeTheme: { name: 'uniform', value: 0.13, factor: 1 } as SizeThemeProps,
         visible: true,
         alpha: 1,
         depthMask: true,
         useFog: true,
         quality: 'auto' as VisualQuality,
-        unitKinds: [] as Unit.Kind[]
+        unitKinds: [] as Unit.Kind[],
+        linearSegments: 8,
+        radialSegments: 12,
+        aspectRatio: 8,
+        arrowFactor: 1.5
     }
 
     componentWillMount() {
@@ -68,7 +68,6 @@ export class Cartoon extends View<Controller<any>, CartoonState, { transform: Ca
     }
 
     update(state?: Partial<CartoonState>) {
-        console.log(state)
         const { transform, entity, ctx } = this.props
         const newState = { ...this.state, ...state }
         this.setState(newState)
@@ -86,7 +85,7 @@ export class Cartoon extends View<Controller<any>, CartoonState, { transform: Ca
             return <option key={value} value={value}>{value.toString()}</option>
         })
 
-        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+        const colorThemeOptions = ColorThemeNames.map((name, idx) => {
             return <option key={name} value={name}>{name}</option>
         })
 
@@ -137,19 +136,12 @@ export class Cartoon extends View<Controller<any>, CartoonState, { transform: Ca
                                     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 }
-                                            })
-                                        }
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
                                     }}
                                 >
                                     {colorThemeOptions}
@@ -235,6 +227,49 @@ export class Cartoon extends View<Controller<any>, CartoonState, { transform: Ca
                                 />
                             </div>
                         </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.aspectRatio || 1}
+                                    label='Aspect ratio'
+                                    min={0.1}
+                                    max={10}
+                                    step={0.1}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({ aspectRatio: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.value || 0.1}
+                                    label='Size value'
+                                    min={0.01}
+                                    max={0.3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, value: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.factor || 1}
+                                    label='Size factor'
+                                    min={0.1}
+                                    max={3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, factor: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>

+ 13 - 28
src/mol-app/ui/transform/distance-restraint.tsx

@@ -15,28 +15,20 @@ 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 { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
 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
+    colorTheme: ColorThemeProps
     colorValue: Color
-    sizeTheme: SizeTheme
+    sizeTheme: SizeThemeProps
     visible: boolean
     alpha: number
     depthMask: boolean
@@ -55,9 +47,9 @@ export class DistanceRestraint extends View<Controller<any>, DistanceRestraintSt
         doubleSided: true,
         flipSided: false,
         flatShaded: false,
-        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
         colorValue: 0x000000,
-        sizeTheme: { name: 'uniform' } as SizeTheme,
+        sizeTheme: { name: 'uniform' } as SizeThemeProps,
         visible: true,
         alpha: 1,
         depthMask: true,
@@ -89,7 +81,7 @@ export class DistanceRestraint extends View<Controller<any>, DistanceRestraintSt
             return <option key={name} value={name}>{name}</option>
         })
 
-        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+        const colorThemeOptions = ColorThemeNames.map((name, idx) => {
             return <option key={name} value={name}>{name}</option>
         })
 
@@ -128,19 +120,12 @@ export class DistanceRestraint extends View<Controller<any>, DistanceRestraintSt
                                     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 }
-                                            })
-                                        }
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
                                     }}
                                 >
                                     {colorThemeOptions}

+ 8 - 8
src/mol-app/ui/transform/file-loader.tsx

@@ -11,19 +11,19 @@ import { TransformListController } from '../../controller/transform/list';
 import { FileEntity } from 'mol-view/state/entity';
 import { MmcifFileToModel, ModelToStructure, StructureToBallAndStick, StructureToSpacefill, StructureToDistanceRestraint, StructureToBackbone } from 'mol-view/state/transform';
 import { StateContext } from 'mol-view/state/context';
-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 { BackboneProps } from 'mol-geo/representation/structure/backbone';
+import { SpacefillProps } from 'mol-geo/representation/structure/representation/spacefill';
+import { BallAndStickProps } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/representation/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/representation/backbone';
 
-const spacefillProps: SpacefillProps = {
+const spacefillProps: Partial<SpacefillProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',
     useFog: false
 }
 
-const ballAndStickProps: BallAndStickProps = {
+const ballAndStickProps: Partial<BallAndStickProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     sizeTheme: { name: 'uniform', value: 0.05 },
@@ -32,7 +32,7 @@ const ballAndStickProps: BallAndStickProps = {
     useFog: false
 }
 
-const distanceRestraintProps: DistanceRestraintProps = {
+const distanceRestraintProps: Partial<DistanceRestraintProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     linkRadius: 0.5,
@@ -40,7 +40,7 @@ const distanceRestraintProps: DistanceRestraintProps = {
     useFog: false
 }
 
-const backboneProps: BackboneProps = {
+const backboneProps: Partial<BackboneProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',

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

@@ -23,6 +23,7 @@ import { Cartoon } from './cartoon';
 import { DistanceRestraint } from './distance-restraint';
 import { Backbone } from './backbone';
 import { UrlLoader } from './url-loader';
+import { Carbohydrate } from './carbohydrate';
 
 function getTransformComponent(controller: TransformListController, entity: AnyEntity, transform: AnyTransform) {
     switch (transform.kind) {
@@ -44,6 +45,8 @@ function getTransformComponent(controller: TransformListController, entity: AnyE
             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>
+        case 'carbohydrate-update':
+            return <Carbohydrate controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Carbohydrate>
     }
     return <Transform controller={controller} entity={entity} transform={transform}></Transform>
 }

+ 28 - 29
src/mol-app/ui/transform/spacefill.tsx

@@ -15,29 +15,21 @@ import { Toggle } from '../controls/common';
 import { SpacefillEntity } from 'mol-view/state/entity';
 import { SpacefillUpdate } from 'mol-view/state/transform'
 import { StateContext } from 'mol-view/state/context';
-import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { ColorThemeProps, ColorThemeNames, ColorThemeName } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
 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 SpacefillState {
     doubleSided: boolean
     flipSided: boolean
     flatShaded: boolean
     detail: number
-    colorTheme: ColorTheme
+    colorTheme: ColorThemeProps
     colorValue: Color
-    sizeTheme: SizeTheme
+    sizeTheme: SizeThemeProps
     visible: boolean
     alpha: number
     depthMask: boolean
@@ -52,9 +44,9 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
         flipSided: false,
         flatShaded: false,
         detail: 2,
-        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorTheme: { name: 'element-symbol' } as ColorThemeProps,
         colorValue: 0x000000,
-        sizeTheme: { name: 'uniform' } as SizeTheme,
+        sizeTheme: { name: 'uniform', factor: 1 } as SizeThemeProps,
         visible: true,
         alpha: 1,
         depthMask: true,
@@ -68,7 +60,6 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
     }
 
     update(state?: Partial<SpacefillState>) {
-        console.log(state)
         const { transform, entity, ctx } = this.props
         const newState = { ...this.state, ...state }
         this.setState(newState)
@@ -86,7 +77,7 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
             return <option key={value} value={value}>{value.toString()}</option>
         })
 
-        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+        const colorThemeOptions = ColorThemeNames.map((name, idx) => {
             return <option key={name} value={name}>{name}</option>
         })
 
@@ -137,19 +128,12 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
                                     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 }
-                                            })
-                                        }
+                                        this.update({
+                                            colorTheme: {
+                                                name: e.target.value as ColorThemeName,
+                                                value: this.state.colorValue
+                                            }
+                                        })
                                     }}
                                 >
                                     {colorThemeOptions}
@@ -235,6 +219,21 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
                                 />
                             </div>
                         </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.sizeTheme.factor || 1}
+                                    label='Size factor'
+                                    min={0.1}
+                                    max={3}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({
+                                        sizeTheme: { ...this.state.sizeTheme, factor: value }
+                                    })}
+                                />
+                            </div>
+                        </div>
                     </div>
                 </div>
             </div>

+ 10 - 10
src/mol-app/ui/transform/url-loader.tsx

@@ -10,20 +10,20 @@ import { TransformListController } from '../../controller/transform/list';
 import { UrlEntity } from 'mol-view/state/entity';
 import { ModelToStructure, StructureToBallAndStick, StructureToSpacefill, StructureToDistanceRestraint, StructureToBackbone, MmcifUrlToModel, StructureToCartoon } from 'mol-view/state/transform';
 import { StateContext } from 'mol-view/state/context';
-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 { BackboneProps } from 'mol-geo/representation/structure/backbone';
-import { CartoonProps } from 'mol-geo/representation/structure/cartoon';
+import { SpacefillProps } from 'mol-geo/representation/structure/representation/spacefill';
+import { BallAndStickProps } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/representation/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/representation/backbone';
+import { CartoonProps } from 'mol-geo/representation/structure/representation/cartoon';
 
-const spacefillProps: SpacefillProps = {
+const spacefillProps: Partial<SpacefillProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',
     useFog: false
 }
 
-const ballAndStickProps: BallAndStickProps = {
+const ballAndStickProps: Partial<BallAndStickProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     sizeTheme: { name: 'uniform', value: 0.05 },
@@ -32,7 +32,7 @@ const ballAndStickProps: BallAndStickProps = {
     useFog: false
 }
 
-const distanceRestraintProps: DistanceRestraintProps = {
+const distanceRestraintProps: Partial<DistanceRestraintProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     linkRadius: 0.5,
@@ -40,14 +40,14 @@ const distanceRestraintProps: DistanceRestraintProps = {
     useFog: false
 }
 
-const backboneProps: BackboneProps = {
+const backboneProps: Partial<BackboneProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',
     useFog: false
 }
 
-const cartoonProps: CartoonProps = {
+const cartoonProps: Partial<CartoonProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',

+ 45 - 62
src/mol-geo/primitive/box.ts

@@ -4,72 +4,55 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-// adapted from three.js, MIT License Copyright 2010-2018 three.js authors
-
 import { Vec3 } from 'mol-math/linear-algebra'
-import { Primitive } from './primitive';
-
-export const DefaultBoxProps = {
-    width: 1,
-    height: 1,
-    depth: 1
-}
-export type BoxProps = Partial<typeof DefaultBoxProps>
-
-const tmpVector = Vec3.zero();
-
-export function Box(props?: BoxProps): Primitive {
-    const { width, height, depth } = { ...DefaultBoxProps, ...props }
-
-    // buffers
-    const vertices = new Float32Array(72);
-    const normals = new Float32Array(72);
-    const indices = new Uint32Array(36);
-
-    // helper variables
-    let vertexCount = 0;
-
-    // build each side of the box geometry
-    buildPlane(2, 1, 0, -1, -1, depth, height, width); // px
-    buildPlane(2, 1, 0, 1, -1, depth, height, -width); // nx
-    buildPlane(0, 2, 1, 1, 1, width, depth, height); // py
-    buildPlane(0, 2, 1, 1, -1, width, depth, -height); // ny
-    buildPlane(0, 1, 2, 1, -1, width, height, depth); // pz
-    buildPlane(0, 1, 2, -1, -1, width, height, -depth); // nz
+import { Primitive, PrimitiveBuilder } from './primitive';
+import { polygon } from './polygon'
 
-    return { vertices, normals, indices }
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
+const points = polygon(4, true)
 
-    function buildPlane(u: number, v: number, w: number, udir: number, vdir: number, width: number, height: number, depth: number) {
-        // generate vertices and normals
-        for (let iy = 0; iy < 2; ++iy) {
-            const y = iy * height - height / 2;
-            for (let ix = 0; ix < 2; ++ix) {
-                const x = ix * width - width / 2;
-
-                // set values to correct vector component and add to vertex buffer
-                tmpVector[u] = x * udir;
-                tmpVector[v] = y * vdir;
-                tmpVector[w] = depth / 2;
-                Vec3.toArray(tmpVector, vertices, vertexCount * 3);
+/**
+ * Create a box
+ */
+function createBox(perforated: boolean): Primitive {
+    const builder = PrimitiveBuilder(12)
+
+    // create sides
+    for (let i = 0; i < 4; ++i) {
+        const ni = (i + 1) % 4
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+        Vec3.set(c, points[ni * 2], points[ni * 2 + 1], 0.5)
+        Vec3.set(d, points[i * 2], points[i * 2 + 1], 0.5)
+        builder.add(a, b, c)
+        if (!perforated) builder.add(c, d, a)
+    }
 
-                // set values to correct vector component and add to normal buffer
-                tmpVector[u] = 0;
-                tmpVector[v] = 0;
-                tmpVector[w] = depth > 0 ? 1 : -1;
-                Vec3.toArray(tmpVector, normals, vertexCount * 3);
+    // create bases
+    Vec3.set(a, points[0], points[1], -0.5)
+    Vec3.set(b, points[2], points[3], -0.5)
+    Vec3.set(c, points[4], points[5], -0.5)
+    Vec3.set(d, points[6], points[7], -0.5)
+    builder.add(a, b, c)
+    if (!perforated) builder.add(c, d, a)
+    Vec3.set(a, points[0], points[1], 0.5)
+    Vec3.set(b, points[2], points[3], 0.5)
+    Vec3.set(c, points[4], points[5], 0.5)
+    Vec3.set(d, points[6], points[7], 0.5)
+    builder.add(a, b, c)
+    if (!perforated) builder.add(c, d, a)
+
+    return builder.getPrimitive()
+}
 
-                ++vertexCount;
-            }
-        }
+let box: Primitive
+export function Box() {
+    if (!box) box = createBox(false)
+    return box
+}
 
-        // faces
-        const vc = vertexCount - 4
-        const iidx = (vc / 2) * 3
-        indices[iidx] = vc
-        indices[iidx + 1] = vc + 2
-        indices[iidx + 2] = vc + 1
-        indices[iidx + 3] = vc + 2
-        indices[iidx + 4] = vc + 3
-        indices[iidx + 5] = vc + 1
-    }
+let perforatedBox: Primitive
+export function PerforatedBox() {
+    if (!perforatedBox) perforatedBox = createBox(true)
+    return perforatedBox
 }

+ 9 - 2
src/mol-geo/primitive/octahedron.ts

@@ -10,13 +10,20 @@ export const octahedronVertices: ReadonlyArray<number> = [
     0.5, 0, 0,   -0.5, 0, 0,    0, 0.5, 0,
     0, -0.5, 0,     0, 0, 0.5,  0, 0, -0.5
 ];
-
 export const octahedronIndices: ReadonlyArray<number> = [
     0, 2, 4,  0, 4, 3,  0, 3, 5,
     0, 5, 2,  1, 2, 5,  1, 5, 3,
     1, 3, 4,  1, 4, 2
 ];
+export const perforatedOctahedronIndices: ReadonlyArray<number> = [
+    0, 2, 4,   0, 4, 3,
+    // 0, 3, 5,   0, 5, 2,
+    1, 2, 5,   1, 5, 3,
+    // 1, 3, 4,   1, 4, 2
+];
 
 const octahedron = createPrimitive(octahedronVertices, octahedronIndices)
+const perforatedOctahedron = createPrimitive(octahedronVertices, perforatedOctahedronIndices)
 
-export function Octahedron(): Primitive { return octahedron }
+export function Octahedron(): Primitive { return octahedron }
+export function PerforatedOctahedron(): Primitive { return perforatedOctahedron }

+ 12 - 47
src/mol-geo/primitive/prism.ts

@@ -12,12 +12,13 @@ const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5)
 const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
 
 /**
- * Create a prism with a poligonal base
+ * Create a prism with a poligonal base of 5 or more points
  */
 export function Prism(points: ArrayLike<number>): Primitive {
     const sideCount = points.length / 2
-    const baseCount = sideCount === 3 ? 1 : sideCount === 4 ? 2 : sideCount
-    const count = 2 * baseCount + 2 * sideCount
+    if (sideCount < 5) throw new Error('need at least 5 points to build a prism')
+
+    const count = 4 * sideCount
     const builder = PrimitiveBuilder(count)
 
     // create sides
@@ -32,55 +33,19 @@ export function Prism(points: ArrayLike<number>): Primitive {
     }
 
     // create bases
-    if (sideCount === 3) {
-        Vec3.set(a, points[0], points[1], -0.5)
-        Vec3.set(b, points[2], points[3], -0.5)
-        Vec3.set(c, points[4], points[5], -0.5)
-        builder.add(a, b, c)
-        Vec3.set(a, points[0], points[1], 0.5)
-        Vec3.set(b, points[2], points[3], 0.5)
-        Vec3.set(c, points[4], points[5], 0.5)
-        builder.add(c, b, a)
-    } else if (sideCount === 4) {
-        Vec3.set(a, points[0], points[1], -0.5)
-        Vec3.set(b, points[2], points[3], -0.5)
-        Vec3.set(c, points[4], points[5], -0.5)
-        Vec3.set(d, points[6], points[7], -0.5)
-        builder.add(a, b, c)
-        builder.add(c, d, a)
-        Vec3.set(a, points[0], points[1], 0.5)
-        Vec3.set(b, points[2], points[3], 0.5)
-        Vec3.set(c, points[4], points[5], 0.5)
-        Vec3.set(d, points[6], points[7], 0.5)
-        builder.add(a, b, c)
-        builder.add(c, d, a)
-    } else {
-        for (let i = 0; i < sideCount; ++i) {
-            const ni = (i + 1) % sideCount
-            Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
-            Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
-            builder.add(a, b, on)
-            Vec3.set(a, points[i * 2], points[i * 2 + 1], 0.5)
-            Vec3.set(b, points[ni * 2], points[ni * 2 + 1], 0.5)
-            builder.add(op, b, a)
-        }
+    for (let i = 0; i < sideCount; ++i) {
+        const ni = (i + 1) % sideCount
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+        builder.add(a, b, on)
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], 0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], 0.5)
+        builder.add(op, b, a)
     }
 
     return builder.getPrimitive()
 }
 
-let wedge: Primitive
-export function Wedge() {
-    if (!wedge) wedge = Prism(polygon(3, false))
-    return wedge
-}
-
-let box: Primitive
-export function Box() {
-    if (!box) box = Prism(polygon(4, true))
-    return box
-}
-
 let diamond: Primitive
 export function DiamondPrism() {
     if (!diamond) diamond = Prism(polygon(4, false))

+ 28 - 1
src/mol-geo/primitive/pyramid.ts

@@ -5,7 +5,7 @@
  */
 
 import { Vec3 } from 'mol-math/linear-algebra'
-import { Primitive, PrimitiveBuilder } from './primitive';
+import { Primitive, PrimitiveBuilder, createPrimitive } from './primitive';
 import { polygon } from './polygon'
 
 const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5)
@@ -57,4 +57,31 @@ let octagonalPyramide: Primitive
 export function OctagonalPyramide() {
     if (!octagonalPyramide) octagonalPyramide = Pyramide(polygon(8, true))
     return octagonalPyramide
+}
+
+//
+
+let perforatedOctagonalPyramide: Primitive
+export function PerforatedOctagonalPyramide() {
+    if (!perforatedOctagonalPyramide) {
+        const points = polygon(8, true)
+        const vertices = new Float32Array(8 * 3 + 6)
+        for (let i = 0; i < 8; ++i) {
+            vertices[i * 3] = points[i * 2]
+            vertices[i * 3 + 1] = points[i * 2 + 1]
+            vertices[i * 3 + 2] = -0.5
+        }
+        vertices[8 * 3] = 0
+        vertices[8 * 3 + 1] = 0
+        vertices[8 * 3 + 2] = -0.5
+        vertices[8 * 3 + 3] = 0
+        vertices[8 * 3 + 4] = 0
+        vertices[8 * 3 + 5] = 0.5
+        const indices: ReadonlyArray<number> = [
+            0, 1, 8,  1, 2, 8,  4, 5, 8,  5, 6, 8,
+            2, 3, 9,  3, 4, 9,  6, 7, 9,  7, 0, 9
+        ];
+        perforatedOctagonalPyramide = createPrimitive(vertices, indices)
+    }
+    return perforatedOctagonalPyramide
 }

+ 6 - 37
src/mol-geo/primitive/sheet.ts

@@ -20,15 +20,10 @@ const positionVector = Vec3.zero()
 const normalVector = Vec3.zero()
 const torsionVector = Vec3.zero()
 
-const arrowVerticalVector = Vec3.zero()
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
 const p3 = Vec3.zero()
 const p4 = Vec3.zero()
-const p5 = Vec3.zero()
-const p6 = Vec3.zero()
-const p7 = Vec3.zero()
-const p8 = Vec3.zero()
 
 export function addSheet(controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) {
     const { vertices, normals, indices } = state
@@ -121,7 +116,7 @@ export function addSheet(controlPoints: ArrayLike<number>, normalVectors: ArrayL
         vertexCount = vertices.elementCount
 
         Vec3.fromArray(verticalVector, normalVectors, offset)
-        Vec3.scale(verticalVector, verticalVector, height);
+        Vec3.scale(verticalVector, verticalVector, arrowHeight === 0 ? height : arrowHeight);
 
         Vec3.fromArray(horizontalVector, binormalVectors, offset)
         Vec3.scale(horizontalVector, horizontalVector, width);
@@ -140,36 +135,11 @@ export function addSheet(controlPoints: ArrayLike<number>, normalVectors: ArrayL
 
         Vec3.cross(normalVector, horizontalVector, verticalVector)
 
-        if (arrowHeight === 0) {
-            for (let i = 0; i < 4; ++i) {
-                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
-            }
-
-            ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-            ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
-        } else {
-            Vec3.fromArray(arrowVerticalVector, normalVectors, offset)
-            Vec3.scale(arrowVerticalVector, verticalVector, arrowHeight);
-
-            Vec3.add(p5, Vec3.add(p5, positionVector, horizontalVector), arrowVerticalVector)
-            Vec3.sub(p6, Vec3.add(p6, positionVector, horizontalVector), arrowVerticalVector)
-            Vec3.sub(p7, Vec3.sub(p7, positionVector, horizontalVector), arrowVerticalVector)
-            Vec3.add(p8, Vec3.sub(p8, positionVector, horizontalVector), arrowVerticalVector)
-
-            ChunkedArray.add3(vertices, p5[0], p5[1], p5[2])
-            ChunkedArray.add3(vertices, p6[0], p6[1], p6[2])
-            ChunkedArray.add3(vertices, p7[0], p7[1], p7[2])
-            ChunkedArray.add3(vertices, p8[0], p8[1], p8[2])
-
-            for (let i = 0; i < 8; ++i) {
-                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
-            }
-
-            ChunkedArray.add3(indices, vertexCount + 7, vertexCount, vertexCount + 4);
-            ChunkedArray.add3(indices, vertexCount + 7, vertexCount + 3, vertexCount);
-            ChunkedArray.add3(indices, vertexCount + 5, vertexCount + 1, vertexCount + 6);
-            ChunkedArray.add3(indices, vertexCount + 1, vertexCount + 2, vertexCount + 6);
+        for (let i = 0; i < 4; ++i) {
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
         }
+        ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+        ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
     }
 
     if (endCap && arrowHeight === 0) {
@@ -199,10 +169,9 @@ export function addSheet(controlPoints: ArrayLike<number>, normalVectors: ArrayL
         for (let i = 0; i < 4; ++i) {
             ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
         }
-
         ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
         ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
     }
 
-    return (linearSegments + 1) * 8 + (startCap ? (arrowHeight === 0 ? 4 : 8) : 0) + (endCap && arrowHeight === 0 ? 4 : 0)
+    return (linearSegments + 1) * 8 + (startCap ? 4 : 0) + (endCap && arrowHeight === 0 ? 4 : 0)
 }

+ 13 - 13
src/mol-geo/primitive/tube.ts

@@ -80,7 +80,7 @@ export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLi
 
     if (startCap) {
         const offset = 0
-        vertexCount = vertices.elementCount
+        const centerVertex = vertices.elementCount
         Vec3.fromArray(u, normalVectors, offset)
         Vec3.fromArray(v, binormalVectors, offset)
         Vec3.fromArray(controlPoint, controlPoints, offset)
@@ -89,6 +89,7 @@ export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLi
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
+        vertexCount = vertices.elementCount
         for (let i = 0; i < radialSegments; ++i) {
             const t = 2 * Math.PI * i / radialSegments;
 
@@ -106,16 +107,16 @@ export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLi
 
             ChunkedArray.add3(
                 indices,
-                vertexCount,
-                vertexCount + i + 1,
-                vertexCount + (i + 1) % radialSegments + 1
+                centerVertex,
+                vertexCount + i,
+                vertexCount + (i + 1) % radialSegments
             );
         }
     }
 
     if (endCap) {
         const offset = linearSegments * 3
-        vertexCount = vertices.elementCount
+        const centerVertex = vertices.elementCount
         Vec3.fromArray(u, normalVectors, offset)
         Vec3.fromArray(v, binormalVectors, offset)
         Vec3.fromArray(controlPoint, controlPoints, offset)
@@ -124,6 +125,7 @@ export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLi
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
+        vertexCount = vertices.elementCount
         for (let i = 0; i < radialSegments; ++i) {
             const t = 2 * Math.PI * i / radialSegments;
 
@@ -139,14 +141,12 @@ export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLi
             ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
             ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
-            if (i < radialSegments - 2) {
-                ChunkedArray.add3(
-                    indices,
-                    vertexCount + i + 1,
-                    vertexCount + (i + 1) % radialSegments + 1,
-                    vertexCount
-                );
-            }
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i,
+                vertexCount + (i + 1) % radialSegments,
+                centerVertex
+            );
         }
     }
 

+ 37 - 111
src/mol-geo/primitive/wedge.ts

@@ -5,118 +5,44 @@
  */
 
 import { Vec3 } from 'mol-math/linear-algebra'
-import { Primitive } from './primitive';
+import { Primitive, PrimitiveBuilder } from './primitive';
+import { polygon } from './polygon'
 
-export const DefaultWedgeProps = {
-    width: 1,
-    height: 1,
-    depth: 1
-}
-export type WedgeProps = Partial<typeof DefaultWedgeProps>
-
-const _a = Vec3.create(0, 0.5, 0.5)
-const _b = Vec3.create(0.5, -0.5, 0.5)
-const _c = Vec3.create(-0.5, -0.5, 0.5)
-const _d = Vec3.create(0, 0.5, -0.5)
-const _e = Vec3.create(0.5, -0.5, -0.5)
-const _f = Vec3.create(-0.5, -0.5, -0.5)
-
-const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero()
-const d = Vec3.zero(), e = Vec3.zero(), f = Vec3.zero()
-
-const nabc = Vec3.create(0, 0, 1)
-const ndef = Vec3.create(0, 0, -1)
-const nabde = Vec3.zero()
-const nbcef = Vec3.create(0, -1, 0)
-const nacdf = Vec3.zero()
-
-const s = Vec3.zero()
-
-export function Wedge(props?: WedgeProps): Primitive {
-    const { width, height, depth } = { ...DefaultWedgeProps, ...props }
-
-    const vertices = new Float32Array(54)
-    const normals = new Float32Array(54)
-    const indices = new Uint32Array(24)
-
-    Vec3.set(s, width, height, depth)
-    Vec3.mul(a, _a, s); Vec3.mul(b, _b, s); Vec3.mul(c, _c, s)
-    Vec3.mul(d, _d, s); Vec3.mul(e, _e, s); Vec3.mul(f, _f, s)
-
-    Vec3.sub(nabde, b, a)
-    Vec3.normalize(nabde, Vec3.set(nabde, -nabde[1], nabde[0], 0))
-    Vec3.sub(nacdf, c, a)
-    Vec3.normalize(nacdf, Vec3.set(nacdf, nacdf[1], -nacdf[0], 0))
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
+const points = polygon(3, false)
 
-    let vc = 0
-    let ic = 0
-
-    // abc
-    Vec3.toArray(a, vertices, vc + 0)
-    Vec3.toArray(c, vertices, vc + 3)
-    Vec3.toArray(b, vertices, vc + 6)
-    for (let i = 0; i < 3; ++i) Vec3.toArray(nabc, normals, vc + i * 3)
-    indices[ic + 0] = vc / 3 + 0
-    indices[ic + 1] = vc / 3 + 1
-    indices[ic + 2] = vc / 3 + 2
-    vc += 9
-    ic += 3
-
-    // def
-    Vec3.toArray(d, vertices, vc + 0)
-    Vec3.toArray(e, vertices, vc + 3)
-    Vec3.toArray(f, vertices, vc + 6)
-    for (let i = 0; i < 3; ++i) Vec3.toArray(ndef, normals, vc + i * 3)
-    indices[ic + 0] = vc / 3 + 0
-    indices[ic + 1] = vc / 3 + 1
-    indices[ic + 2] = vc / 3 + 2
-    vc += 9
-    ic += 3
-
-    // abde
-    Vec3.toArray(a, vertices, vc + 0)
-    Vec3.toArray(d, vertices, vc + 3)
-    Vec3.toArray(e, vertices, vc + 6)
-    Vec3.toArray(b, vertices, vc + 9)
-    for (let i = 0; i < 4; ++i) Vec3.toArray(nabde, normals, vc + i * 3)
-    indices[ic + 0] = vc / 3 + 2
-    indices[ic + 1] = vc / 3 + 1
-    indices[ic + 2] = vc / 3 + 0
-    indices[ic + 3] = vc / 3 + 0
-    indices[ic + 4] = vc / 3 + 3
-    indices[ic + 5] = vc / 3 + 2
-    vc += 12
-    ic += 6
-
-    // acdf
-    Vec3.toArray(d, vertices, vc + 0)
-    Vec3.toArray(a, vertices, vc + 3)
-    Vec3.toArray(c, vertices, vc + 6)
-    Vec3.toArray(f, vertices, vc + 9)
-    for (let i = 0; i < 4; ++i) Vec3.toArray(nacdf, normals, vc + i * 3)
-    indices[ic + 0] = vc / 3 + 2
-    indices[ic + 1] = vc / 3 + 1
-    indices[ic + 2] = vc / 3 + 0
-    indices[ic + 3] = vc / 3 + 0
-    indices[ic + 4] = vc / 3 + 3
-    indices[ic + 5] = vc / 3 + 2
-    vc += 12
-    ic += 6
-
-    // bcef
-    Vec3.toArray(e, vertices, vc + 0)
-    Vec3.toArray(f, vertices, vc + 3)
-    Vec3.toArray(c, vertices, vc + 6)
-    Vec3.toArray(b, vertices, vc + 9)
-    for (let i = 0; i < 4; ++i) Vec3.toArray(nbcef, normals, vc + i * 3)
-    indices[ic + 0] = vc / 3 + 2
-    indices[ic + 1] = vc / 3 + 1
-    indices[ic + 2] = vc / 3 + 0
-    indices[ic + 3] = vc / 3 + 0
-    indices[ic + 4] = vc / 3 + 3
-    indices[ic + 5] = vc / 3 + 2
-    vc += 12
-    ic += 6
+/**
+ * Create a prism with a poligonal base
+ */
+export function createWedge(): Primitive {
+    const builder = PrimitiveBuilder(8)
+
+    // create sides
+    for (let i = 0; i < 3; ++i) {
+        const ni = (i + 1) % 3
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+        Vec3.set(c, points[ni * 2], points[ni * 2 + 1], 0.5)
+        Vec3.set(d, points[i * 2], points[i * 2 + 1], 0.5)
+        builder.add(a, b, c)
+        builder.add(c, d, a)
+    }
+
+    // create bases
+    Vec3.set(a, points[0], points[1], -0.5)
+    Vec3.set(b, points[2], points[3], -0.5)
+    Vec3.set(c, points[4], points[5], -0.5)
+    builder.add(a, b, c)
+    Vec3.set(a, points[0], points[1], 0.5)
+    Vec3.set(b, points[2], points[3], 0.5)
+    Vec3.set(c, points[4], points[5], 0.5)
+    builder.add(c, b, a)
+
+    return builder.getPrimitive()
+}
 
-    return { vertices, normals, indices }
+let wedge: Primitive
+export function Wedge() {
+    if (!wedge) wedge = createWedge()
+    return wedge
 }

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

@@ -15,8 +15,8 @@ export interface RepresentationProps {}
 export interface Representation<D, P extends RepresentationProps = {}> {
     readonly renderObjects: ReadonlyArray<RenderObject>
     readonly props: Readonly<P>
-    create: (data: D, props?: P) => Task<void>
-    update: (props: P) => Task<void>
+    create: (data: D, props?: Partial<P>) => Task<void>
+    update: (props: Partial<P>) => Task<void>
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => void
     destroy: () => void
@@ -24,8 +24,8 @@ export interface Representation<D, P extends RepresentationProps = {}> {
 
 export interface Visual<D, P extends RepresentationProps = {}> {
     readonly renderObject: RenderObject
-    create: (ctx: RuntimeContext, data: D, props: P) => Promise<void>
-    update: (ctx: RuntimeContext, props: P) => Promise<boolean>
+    create: (ctx: RuntimeContext, data: D, props?: Partial<P>) => Promise<void>
+    update: (ctx: RuntimeContext, props: Partial<P>) => Promise<boolean>
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => void
     destroy: () => void

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

@@ -1,88 +0,0 @@
-/**
- * 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, isEmptyLoci } from 'mol-model/loci';
-import { MarkerAction } from '../../util/marker-data';
-import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh';
-import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder';
-import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from './visual/nucleotide-block-mesh';
-import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from './visual/polymer-direction-wedge';
-import { CarbohydrateSymbolVisual } from './visual/carbohydrate-symbol-mesh';
-
-export const DefaultCartoonProps = {
-    ...DefaultPolymerTraceProps,
-    ...DefaultPolymerGapProps,
-    ...DefaultNucleotideBlockProps,
-    ...DefaultPolymerDirectionProps
-}
-export type CartoonProps = Partial<typeof DefaultCartoonProps>
-
-export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
-    const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual)
-    const gapRepr = StructureUnitsRepresentation(PolymerGapVisual)
-    const blockRepr = StructureUnitsRepresentation(NucleotideBlockVisual)
-    const directionRepr = StructureUnitsRepresentation(PolymerDirectionVisual)
-    const carbohydrateRepr = StructureRepresentation(CarbohydrateSymbolVisual)
-
-    return {
-        get renderObjects() {
-            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects, ...directionRepr.renderObjects, ...carbohydrateRepr.renderObjects ]
-        },
-        get props() {
-            return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props, ...carbohydrateRepr.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)
-                await gapRepr.create(structure, p).runInContext(ctx)
-                await blockRepr.create(structure, p).runInContext(ctx)
-                await directionRepr.create(structure, p).runInContext(ctx)
-                await carbohydrateRepr.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)
-                await gapRepr.update(p).runInContext(ctx)
-                await blockRepr.update(p).runInContext(ctx)
-                await directionRepr.update(p).runInContext(ctx)
-                await carbohydrateRepr.update(p).runInContext(ctx)
-            })
-        },
-        getLoci: (pickingId: PickingId) => {
-            const traceLoci = traceRepr.getLoci(pickingId)
-            const gapLoci = gapRepr.getLoci(pickingId)
-            const blockLoci = blockRepr.getLoci(pickingId)
-            const directionLoci = directionRepr.getLoci(pickingId)
-            const carbohydrateRepr = directionRepr.getLoci(pickingId)
-            return !isEmptyLoci(traceLoci) ? traceLoci
-                : !isEmptyLoci(gapLoci) ? gapLoci
-                : !isEmptyLoci(blockLoci) ? blockLoci
-                : !isEmptyLoci(directionLoci) ? directionLoci
-                : carbohydrateRepr
-        },
-        mark: (loci: Loci, action: MarkerAction) => {
-            traceRepr.mark(loci, action)
-            gapRepr.mark(loci, action)
-            blockRepr.mark(loci, action)
-            directionRepr.mark(loci, action)
-            carbohydrateRepr.mark(loci, action)
-        },
-        destroy() {
-            traceRepr.destroy()
-            gapRepr.destroy()
-            blockRepr.destroy()
-            directionRepr.destroy()
-            carbohydrateRepr.destroy()
-        }
-    }
-}

+ 79 - 0
src/mol-geo/representation/structure/complex-representation.ts

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task'
+import { PickingId } from '../../util/picking';
+import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { getQualityProps } from '../util';
+import { StructureProps, DefaultStructureProps, StructureRepresentation } from '.';
+import { ComplexVisual } from './complex-visual';
+
+export function ComplexRepresentation<P extends StructureProps>(visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
+    let visual: ComplexVisual<P>
+
+    let _props: P
+    let _structure: Structure
+
+    function create(structure: Structure, props: Partial<P> = {}) {
+        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
+        _props.colorTheme!.structure = structure
+
+        return Task.create('Creating StructureRepresentation', async ctx => {
+            if (!_structure) {
+                visual = visualCtor()
+                await visual.create(ctx, structure, _props)
+            } else {
+                if (_structure.hashCode === structure.hashCode) {
+                    await update(_props)
+                } else {
+                    if (!await visual.update(ctx, _props)) {
+                        await visual.create(ctx, structure, _props)
+                    }
+                }
+            }
+            _structure = structure
+        });
+    }
+
+    function update(props: Partial<P>) {
+        return Task.create('Updating StructureRepresentation', async ctx => {
+            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
+            _props.colorTheme!.structure = _structure
+
+            if (!await visual.update(ctx, _props)) {
+                await visual.create(ctx, _structure, _props)
+            }
+        })
+    }
+
+    function getLoci(pickingId: PickingId) {
+        let loci: Loci = EmptyLoci
+        const _loci = visual.getLoci(pickingId)
+        if (!isEmptyLoci(_loci)) loci = _loci
+        return loci
+    }
+
+    function mark(loci: Loci, action: MarkerAction) {
+        visual.mark(loci, action)
+    }
+
+    function destroy() {
+        visual.destroy()
+    }
+
+    return {
+        get renderObjects() { return [ visual.renderObject ] },
+        get props() { return _props },
+        create,
+        update,
+        getLoci,
+        mark,
+        destroy
+    }
+}

+ 122 - 0
src/mol-geo/representation/structure/complex-visual.ts

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { Visual } from '..';
+import { MeshRenderObject } from 'mol-gl/render-object';
+import { Mesh } from '../../shape/mesh';
+import { RuntimeContext } from 'mol-task';
+import { LocationIterator } from './visual/util/location-iterator';
+import { createComplexMeshRenderObject, createColors } from './visual/util/common';
+import { StructureProps, DefaultStructureMeshProps, MeshUpdateState } from '.';
+import { deepEqual, ValueCell } from 'mol-util';
+import { updateMeshValues, updateRenderableState } from '../util';
+import { PickingId } from '../../util/picking';
+import { Loci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
+import { Interval } from 'mol-data/int';
+
+export interface  ComplexVisual<P extends StructureProps> extends Visual<Structure, P> { }
+
+export const DefaultComplexMeshProps = {
+    ...DefaultStructureMeshProps
+}
+export type ComplexMeshProps = typeof DefaultComplexMeshProps
+
+export interface ComplexMeshVisualBuilder<P extends ComplexMeshProps> {
+    defaultProps: P
+    createMesh(ctx: RuntimeContext, structure: Structure, props: P, mesh?: Mesh): Promise<Mesh>
+    createLocationIterator(structure: Structure): LocationIterator
+    getLoci(pickingId: PickingId, structure: Structure, id: number): Loci
+    mark(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean): boolean,
+    setUpdateState(state: MeshUpdateState, newProps: P, currentProps: P): void
+}
+
+export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMeshVisualBuilder<P>): ComplexVisual<P> {
+    const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const updateState = MeshUpdateState.create()
+
+    let renderObject: MeshRenderObject
+    let currentProps: P
+    let mesh: Mesh
+    let currentStructure: Structure
+    let locationIt: LocationIterator
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) {
+            currentProps = Object.assign({}, defaultProps, props)
+            currentStructure = structure
+
+            mesh = await createMesh(ctx, currentStructure, currentProps, mesh)
+
+            locationIt = createLocationIterator(structure)
+            renderObject = createComplexMeshRenderObject(structure, mesh, locationIt, currentProps)
+        },
+        async update(ctx: RuntimeContext, props: Partial<P>) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            locationIt.reset()
+            MeshUpdateState.reset(updateState)
+            setUpdateState(updateState, newProps, currentProps)
+
+            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
+                updateState.createMesh = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateState.updateColor = true
+            }
+
+            //
+
+            if (updateState.createMesh) {
+                mesh = await createMesh(ctx, currentStructure, newProps, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateState.updateColor = true
+            }
+
+            if (updateState.updateColor) {
+                createColors(locationIt, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getLoci(pickingId, currentStructure, renderObject.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            const { tMarker } = renderObject.values
+            const { elementCount, instanceCount } = locationIt
+
+            function apply(interval: Interval) {
+                const start = Interval.start(interval)
+                const end = Interval.end(interval)
+                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
+            }
+
+            let changed = false
+            if (isEveryLoci(loci)) {
+                apply(Interval.ofBounds(0, elementCount * instanceCount))
+                changed = true
+            } else {
+                changed = mark(loci, currentStructure, apply)
+            }
+            if (changed) {
+                ValueCell.update(tMarker, tMarker.ref.value)
+            }
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 30 - 177
src/mol-geo/representation/structure/index.ts

@@ -5,192 +5,45 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Structure, Unit } from 'mol-model/structure';
-import { Task } from 'mol-task'
-import { RenderObject } from 'mol-gl/render-object';
-import { Representation, RepresentationProps, Visual } from '..';
-import { ColorTheme, SizeTheme } from '../../theme';
-import { PickingId } from '../../util/picking';
-import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
-import { MarkerAction } from '../../util/marker-data';
-import { getQualityProps, DefaultBaseProps } from '../util';
-
-export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
-export interface  StructureVisual<P extends RepresentationProps = {}> extends Visual<Structure, P> { }
+import { Structure } from 'mol-model/structure';
+import { ColorThemeProps } from 'mol-view/theme/color';
+import { SizeThemeProps } from 'mol-view/theme/size';
+import { Representation, RepresentationProps } from '..';
+import { DefaultBaseProps, DefaultMeshProps } from '../util';
 
 export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { }
 
 export const DefaultStructureProps = {
     ...DefaultBaseProps,
-    colorTheme: { name: 'instance-index' } as ColorTheme,
-    sizeTheme: { name: 'physical' } as SizeTheme,
+    colorTheme: { name: 'unit-index' } as ColorThemeProps,
+    sizeTheme: { name: 'physical' } as SizeThemeProps,
 }
-export type StructureProps = Partial<typeof DefaultStructureProps>
-
-export function StructureRepresentation<P extends StructureProps>(visualCtor: () => StructureVisual<P>): StructureRepresentation<P> {
-    let visual: StructureVisual<P>
-
-    let _props: Required<P>
-    let _structure: Structure
-
-    function create(structure: Structure, props: P = {} as P) {
-        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
-
-        return Task.create('Creating StructureRepresentation', async ctx => {
-            if (!_structure) {
-                visual = visualCtor()
-                await visual.create(ctx, structure, _props)
-            } else {
-                if (_structure.hashCode === structure.hashCode) {
-                    await update(_props)
-                } else {
-                    if (!await visual.update(ctx, _props)) {
-                        await visual.create(ctx, _structure, _props)
-                    }
-                }
-            }
-            _structure = structure
-        });
-    }
-
-    function update(props: P) {
-        return Task.create('Updating StructureRepresentation', async ctx => {
-            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
-
-            if (!await visual.update(ctx, _props)) {
-                await visual.create(ctx, _structure, _props)
-            }
-        })
-    }
-
-    function getLoci(pickingId: PickingId) {
-        let loci: Loci = EmptyLoci
-        const _loci = visual.getLoci(pickingId)
-        if (!isEmptyLoci(_loci)) loci = _loci
-        return loci
-    }
-
-    function mark(loci: Loci, action: MarkerAction) {
-        visual.mark(loci, action)
-    }
-
-    function destroy() {
-        visual.destroy()
-    }
+export type StructureProps = typeof DefaultStructureProps
 
-    return {
-        get renderObjects() { return [ visual.renderObject ] },
-        get props() { return _props },
-        create,
-        update,
-        getLoci,
-        mark,
-        destroy
-    }
+export const DefaultStructureMeshProps = {
+    ...DefaultStructureProps,
+    ...DefaultMeshProps
 }
+export type StructureMeshProps = typeof DefaultStructureMeshProps
 
-export function StructureUnitsRepresentation<P extends StructureProps>(visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
-    let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
-
-    let _props: Required<P>
-    let _structure: Structure
-    let _groups: ReadonlyArray<Unit.SymmetryGroup>
-
-    function create(structure: Structure, props: P = {} as P) {
-        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
-
-        return Task.create('Creating StructureRepresentation', async ctx => {
-            if (!_structure) {
-                _groups = structure.unitSymmetryGroups;
-                for (let i = 0; i < _groups.length; i++) {
-                    const group = _groups[i];
-                    const visual = visualCtor()
-                    await visual.create(ctx, group, _props)
-                    visuals.set(group.hashCode, { visual, group })
-                }
-            } else {
-                if (_structure.hashCode === structure.hashCode) {
-                    await update(_props)
-                } else {
-                    _groups = structure.unitSymmetryGroups;
-                    const newGroups: Unit.SymmetryGroup[] = []
-                    const oldUnitsVisuals = visuals
-                    visuals = new Map()
-                    for (let i = 0; i < _groups.length; i++) {
-                        const group = _groups[i];
-                        const visualGroup = oldUnitsVisuals.get(group.hashCode)
-                        if (visualGroup) {
-                            const { visual, group } = visualGroup
-                            if (!await visual.update(ctx, _props)) {
-                                await visual.create(ctx, group, _props)
-                            }
-                            oldUnitsVisuals.delete(group.hashCode)
-                        } else {
-                            newGroups.push(group)
-                            const visual = visualCtor()
-                            await visual.create(ctx, group, _props)
-                            visuals.set(group.hashCode, { visual, group })
-                        }
-                    }
-
-                    // for new groups, reuse leftover visuals
-                    const unusedVisuals: UnitsVisual<P>[] = []
-                    oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
-                    newGroups.forEach(async group => {
-                        const visual = unusedVisuals.pop() || visualCtor()
-                        await visual.create(ctx, group, _props)
-                        visuals.set(group.hashCode, { visual, group })
-                    })
-                    unusedVisuals.forEach(visual => visual.destroy())
-                }
-            }
-            _structure = structure
-        });
-    }
-
-    function update(props: P) {
-        return Task.create('Updating StructureRepresentation', async ctx => {
-            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
-
-            visuals.forEach(async ({ visual, group }) => {
-                if (!await visual.update(ctx, _props)) {
-                    await visual.create(ctx, group, _props)
-                }
-            })
-        })
-    }
-
-    function getLoci(pickingId: PickingId) {
-        let loci: Loci = EmptyLoci
-        visuals.forEach(({ visual }) => {
-            const _loci = visual.getLoci(pickingId)
-            if (!isEmptyLoci(_loci)) loci = _loci
-        })
-        return loci
+export interface MeshUpdateState {
+    updateColor: boolean
+    createMesh: boolean
+}
+export namespace MeshUpdateState {
+    export function create(): MeshUpdateState {
+        return {
+            updateColor: false,
+            createMesh: false
+        }
     }
-
-    function mark(loci: Loci, action: MarkerAction) {
-        visuals.forEach(({ visual }) => visual.mark(loci, action))
-    }
-
-    function destroy() {
-        visuals.forEach(({ visual }) => visual.destroy())
-        visuals.clear()
+    export function reset(state: MeshUpdateState) {
+        state.updateColor = false
+        state.createMesh = false
     }
+}
 
-    return {
-        get renderObjects() {
-            const renderObjects: RenderObject[] = []
-            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
-            return renderObjects
-        },
-        get props() {
-            return _props
-        },
-        create,
-        update,
-        getLoci,
-        mark,
-        destroy
-    }
-}
+export { ComplexRepresentation } from './complex-representation'
+export { UnitsRepresentation } from './units-representation'
+export { ComplexVisual } from './complex-visual'
+export { UnitsVisual } from './units-visual'

+ 13 - 12
src/mol-geo/representation/structure/backbone.ts → src/mol-geo/representation/structure/representation/backbone.ts

@@ -4,22 +4,23 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { StructureRepresentation, StructureUnitsRepresentation } from '.';
-import { PickingId } from '../../util/picking';
+import { StructureRepresentation, UnitsRepresentation } 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';
+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 type BackboneProps = typeof DefaultBackboneProps
 
 export function BackboneRepresentation(): StructureRepresentation<BackboneProps> {
-    const traceRepr = StructureUnitsRepresentation(PolymerBackboneVisual)
+    const traceRepr = UnitsRepresentation(PolymerBackboneVisual)
 
+    let currentProps: BackboneProps
     return {
         get renderObjects() {
             return [ ...traceRepr.renderObjects ]
@@ -27,16 +28,16 @@ export function BackboneRepresentation(): StructureRepresentation<BackboneProps>
         get props() {
             return { ...traceRepr.props }
         },
-        create: (structure: Structure, props: BackboneProps = {} as BackboneProps) => {
-            const p = Object.assign({}, DefaultBackboneProps, props)
+        create: (structure: Structure, props: Partial<BackboneProps> = {}) => {
+            currentProps = Object.assign({}, DefaultBackboneProps, props)
             return Task.create('BackboneRepresentation', async ctx => {
-                await traceRepr.create(structure, p).runInContext(ctx)
+                await traceRepr.create(structure, currentProps).runInContext(ctx)
             })
         },
-        update: (props: BackboneProps) => {
-            const p = Object.assign({}, props)
+        update: (props: Partial<BackboneProps>) => {
+            currentProps = Object.assign(currentProps, props)
             return Task.create('Updating BackboneRepresentation', async ctx => {
-                await traceRepr.update(p).runInContext(ctx)
+                await traceRepr.update(currentProps).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {

+ 23 - 22
src/mol-geo/representation/structure/ball-and-stick.ts → src/mol-geo/representation/structure/representation/ball-and-stick.ts

@@ -4,31 +4,32 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { StructureRepresentation, StructureUnitsRepresentation } from '.';
-import { ElementSphereVisual, DefaultElementSphereProps } from './visual/element-sphere';
-import { IntraUnitLinkVisual, DefaultIntraUnitLinkProps } from './visual/intra-unit-link-cylinder';
-import { PickingId } from '../../util/picking';
+import { ComplexRepresentation, StructureRepresentation, UnitsRepresentation } from '..';
+import { ElementSphereVisual, DefaultElementSphereProps } from '../visual/element-sphere';
+import { IntraUnitLinkVisual, DefaultIntraUnitLinkProps } from '../visual/intra-unit-link-cylinder';
+import { PickingId } from '../../../util/picking';
 import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task';
 import { Loci, isEmptyLoci } from 'mol-model/loci';
-import { MarkerAction } from '../../util/marker-data';
-import { SizeTheme } from '../../theme';
-import { InterUnitLinkVisual } from './visual/inter-unit-link-cylinder';
+import { MarkerAction } from '../../../util/marker-data';
+import { InterUnitLinkVisual } from '../visual/inter-unit-link-cylinder';
+import { SizeThemeProps } from 'mol-view/theme/size';
 
 export const DefaultBallAndStickProps = {
     ...DefaultElementSphereProps,
     ...DefaultIntraUnitLinkProps,
 
-    sizeTheme: { name: 'uniform', value: 0.25 } as SizeTheme,
+    sizeTheme: { name: 'uniform', value: 0.25 } as SizeThemeProps,
     unitKinds: [ Unit.Kind.Atomic ] as Unit.Kind[]
 }
-export type BallAndStickProps = Partial<typeof DefaultBallAndStickProps>
+export type BallAndStickProps = typeof DefaultBallAndStickProps
 
 export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> {
-    const elmementRepr = StructureUnitsRepresentation(ElementSphereVisual)
-    const intraLinkRepr = StructureUnitsRepresentation(IntraUnitLinkVisual)
-    const interLinkRepr = StructureRepresentation(InterUnitLinkVisual)
+    const elmementRepr = UnitsRepresentation(ElementSphereVisual)
+    const intraLinkRepr = UnitsRepresentation(IntraUnitLinkVisual)
+    const interLinkRepr = ComplexRepresentation(InterUnitLinkVisual)
 
+    let currentProps: BallAndStickProps
     return {
         get renderObjects() {
             return [ ...elmementRepr.renderObjects, ...intraLinkRepr.renderObjects, ...interLinkRepr.renderObjects ]
@@ -36,20 +37,20 @@ export function BallAndStickRepresentation(): StructureRepresentation<BallAndSti
         get props() {
             return { ...elmementRepr.props, ...intraLinkRepr.props, ...interLinkRepr.props }
         },
-        create: (structure: Structure, props: BallAndStickProps = {} as BallAndStickProps) => {
-            const p = Object.assign({}, DefaultBallAndStickProps, props)
+        create: (structure: Structure, props: Partial<BallAndStickProps> = {}) => {
+            currentProps = Object.assign({}, DefaultBallAndStickProps, props)
             return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await elmementRepr.create(structure, p).runInContext(ctx)
-                await intraLinkRepr.create(structure, p).runInContext(ctx)
-                await interLinkRepr.create(structure, p).runInContext(ctx)
+                await elmementRepr.create(structure, currentProps).runInContext(ctx)
+                await intraLinkRepr.create(structure, currentProps).runInContext(ctx)
+                await interLinkRepr.create(structure, currentProps).runInContext(ctx)
             })
         },
-        update: (props: BallAndStickProps) => {
-            const p = Object.assign({}, props)
+        update: (props: Partial<BallAndStickProps>) => {
+            currentProps = Object.assign(currentProps, props)
             return Task.create('Updating BallAndStickRepresentation', async ctx => {
-                await elmementRepr.update(p).runInContext(ctx)
-                await intraLinkRepr.update(p).runInContext(ctx)
-                await interLinkRepr.update(p).runInContext(ctx)
+                await elmementRepr.update(currentProps).runInContext(ctx)
+                await intraLinkRepr.update(currentProps).runInContext(ctx)
+                await interLinkRepr.update(currentProps).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {

+ 63 - 0
src/mol-geo/representation/structure/representation/carbohydrate.ts

@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ComplexRepresentation, StructureRepresentation } from '..';
+import { PickingId } from '../../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../../util/marker-data';
+import { CarbohydrateSymbolVisual, DefaultCarbohydrateSymbolProps } from '../visual/carbohydrate-symbol-mesh';
+import { CarbohydrateLinkVisual, DefaultCarbohydrateLinkProps } from '../visual/carbohydrate-link-cylinder';
+
+export const DefaultCartoonProps = {
+    ...DefaultCarbohydrateSymbolProps,
+    ...DefaultCarbohydrateLinkProps
+}
+export type CarbohydrateProps = typeof DefaultCartoonProps
+
+export function CarbohydrateRepresentation(): StructureRepresentation<CarbohydrateProps> {
+    const carbohydrateSymbolRepr = ComplexRepresentation(CarbohydrateSymbolVisual)
+    const carbohydrateLinkRepr = ComplexRepresentation(CarbohydrateLinkVisual)
+
+    let currentProps: CarbohydrateProps
+    return {
+        get renderObjects() {
+            return [ ...carbohydrateSymbolRepr.renderObjects, ...carbohydrateLinkRepr.renderObjects ]
+        },
+        get props() {
+            return { ...carbohydrateSymbolRepr.props, ...carbohydrateLinkRepr.props }
+        },
+        create: (structure: Structure, props: Partial<CarbohydrateProps> = {} as CarbohydrateProps) => {
+            currentProps = Object.assign({}, DefaultCartoonProps, props)
+            return Task.create('Creating CarbohydrateRepresentation', async ctx => {
+                await carbohydrateSymbolRepr.create(structure, currentProps).runInContext(ctx)
+                await carbohydrateLinkRepr.create(structure, currentProps).runInContext(ctx)
+            })
+        },
+        update: (props: Partial<CarbohydrateProps>) => {
+            currentProps = Object.assign(currentProps, props)
+            return Task.create('Updating CarbohydrateRepresentation', async ctx => {
+                await carbohydrateSymbolRepr.update(currentProps).runInContext(ctx)
+                await carbohydrateLinkRepr.update(currentProps).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            const carbohydrateSymbolLoci = carbohydrateSymbolRepr.getLoci(pickingId)
+            const carbohydrateLinkLoci = carbohydrateLinkRepr.getLoci(pickingId)
+            return !isEmptyLoci(carbohydrateSymbolLoci) ? carbohydrateSymbolLoci
+                : carbohydrateLinkLoci
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            carbohydrateSymbolRepr.mark(loci, action)
+            carbohydrateLinkRepr.mark(loci, action)
+        },
+        destroy() {
+            carbohydrateSymbolRepr.destroy()
+            carbohydrateLinkRepr.destroy()
+        }
+    }
+}

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

@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, UnitsRepresentation } from '..';
+import { PickingId } from '../../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../../util/marker-data';
+import { PolymerTraceVisual, DefaultPolymerTraceProps } from '../visual/polymer-trace-mesh';
+import { PolymerGapVisual, DefaultPolymerGapProps } from '../visual/polymer-gap-cylinder';
+import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from '../visual/nucleotide-block-mesh';
+import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge';
+
+export const DefaultCartoonProps = {
+    ...DefaultPolymerTraceProps,
+    ...DefaultPolymerGapProps,
+    ...DefaultNucleotideBlockProps,
+    ...DefaultPolymerDirectionProps
+}
+export type CartoonProps = typeof DefaultCartoonProps
+
+export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
+    const traceRepr = UnitsRepresentation(PolymerTraceVisual)
+    const gapRepr = UnitsRepresentation(PolymerGapVisual)
+    const blockRepr = UnitsRepresentation(NucleotideBlockVisual)
+    const directionRepr = UnitsRepresentation(PolymerDirectionVisual)
+
+    let currentProps: CartoonProps
+    return {
+        get renderObjects() {
+            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects,
+                ...blockRepr.renderObjects, ...directionRepr.renderObjects ]
+        },
+        get props() {
+            return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props }
+        },
+        create: (structure: Structure, props: Partial<CartoonProps> = {}) => {
+            currentProps = Object.assign({}, DefaultCartoonProps, props)
+            return Task.create('Creating CartoonRepresentation', async ctx => {
+                await traceRepr.create(structure, currentProps).runInContext(ctx)
+                await gapRepr.create(structure, currentProps).runInContext(ctx)
+                await blockRepr.create(structure, currentProps).runInContext(ctx)
+                await directionRepr.create(structure, currentProps).runInContext(ctx)
+            })
+        },
+        update: (props: Partial<CartoonProps>) => {
+            currentProps = Object.assign(currentProps, props)
+            return Task.create('Updating CartoonRepresentation', async ctx => {
+                await traceRepr.update(currentProps).runInContext(ctx)
+                await gapRepr.update(currentProps).runInContext(ctx)
+                await blockRepr.update(currentProps).runInContext(ctx)
+                await directionRepr.update(currentProps).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            const traceLoci = traceRepr.getLoci(pickingId)
+            const gapLoci = gapRepr.getLoci(pickingId)
+            const blockLoci = blockRepr.getLoci(pickingId)
+            const directionLoci = directionRepr.getLoci(pickingId)
+            return !isEmptyLoci(traceLoci) ? traceLoci
+                : !isEmptyLoci(gapLoci) ? gapLoci
+                : !isEmptyLoci(blockLoci) ? blockLoci
+                : directionLoci
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            traceRepr.mark(loci, action)
+            gapRepr.mark(loci, action)
+            blockRepr.mark(loci, action)
+            directionRepr.mark(loci, action)
+        },
+        destroy() {
+            traceRepr.destroy()
+            gapRepr.destroy()
+            blockRepr.destroy()
+            directionRepr.destroy()
+        }
+    }
+}

+ 15 - 15
src/mol-geo/representation/structure/distance-restraint.ts → src/mol-geo/representation/structure/representation/distance-restraint.ts

@@ -4,25 +4,25 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { StructureRepresentation } from '.';
-import { PickingId } from '../../util/picking';
+import { ComplexRepresentation, StructureRepresentation } 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 { SizeTheme } from '../../theme';
-import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from './visual/cross-link-restraint-cylinder';
+import { MarkerAction } from '../../../util/marker-data';
+import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from '../visual/cross-link-restraint-cylinder';
+import { SizeThemeProps } from 'mol-view/theme/size';
 
 export const DefaultDistanceRestraintProps = {
     ...DefaultCrossLinkRestraintProps,
-
-    sizeTheme: { name: 'uniform', value: 0.25 } as SizeTheme,
+    sizeTheme: { name: 'uniform', value: 0.25 } as SizeThemeProps,
 }
-export type DistanceRestraintProps = Partial<typeof DefaultDistanceRestraintProps>
+export type DistanceRestraintProps = typeof DefaultDistanceRestraintProps
 
 export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> {
-    const crossLinkRepr = StructureRepresentation(CrossLinkRestraintVisual)
+    const crossLinkRepr = ComplexRepresentation(CrossLinkRestraintVisual)
 
+    let currentProps: DistanceRestraintProps
     return {
         get renderObjects() {
             return [ ...crossLinkRepr.renderObjects ]
@@ -30,16 +30,16 @@ export function DistanceRestraintRepresentation(): StructureRepresentation<Dista
         get props() {
             return { ...crossLinkRepr.props }
         },
-        create: (structure: Structure, props: DistanceRestraintProps = {} as DistanceRestraintProps) => {
-            const p = Object.assign({}, DefaultDistanceRestraintProps, props)
+        create: (structure: Structure, props: Partial<DistanceRestraintProps> = {}) => {
+            currentProps = Object.assign({}, DefaultDistanceRestraintProps, props)
             return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.create(structure, p).runInContext(ctx)
+                await crossLinkRepr.create(structure, currentProps).runInContext(ctx)
             })
         },
-        update: (props: DistanceRestraintProps) => {
-            const p = Object.assign({}, props)
+        update: (props: Partial<DistanceRestraintProps>) => {
+            currentProps = Object.assign(currentProps, props)
             return Task.create('Updating DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.update(p).runInContext(ctx)
+                await crossLinkRepr.update(currentProps).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {

+ 5 - 5
src/mol-geo/representation/structure/spacefill.ts → src/mol-geo/representation/structure/representation/spacefill.ts

@@ -4,14 +4,14 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { StructureUnitsRepresentation } from '.';
-import { ElementSphereVisual, DefaultElementSphereProps } from './visual/element-sphere';
+import { UnitsRepresentation } from '..';
+import { ElementSphereVisual, DefaultElementSphereProps } from '../visual/element-sphere';
 
 export const DefaultSpacefillProps = {
-    ...DefaultElementSphereProps,
+    ...DefaultElementSphereProps
 }
-export type SpacefillProps = Partial<typeof DefaultSpacefillProps>
+export type SpacefillProps = typeof DefaultSpacefillProps
 
 export function SpacefillRepresentation() {
-    return StructureUnitsRepresentation(ElementSphereVisual)
+    return UnitsRepresentation(ElementSphereVisual)
 }

+ 129 - 0
src/mol-geo/representation/structure/units-representation.ts

@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Structure, Unit } from 'mol-model/structure';
+import { Task } from 'mol-task'
+import { RenderObject } from 'mol-gl/render-object';
+import { Representation, RepresentationProps, Visual } from '..';
+import { PickingId } from '../../util/picking';
+import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { getQualityProps } from '../util';
+import { DefaultStructureProps, StructureProps } from '.';
+
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export interface  StructureVisual<P extends RepresentationProps = {}> extends Visual<Structure, P> { }
+
+export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { }
+
+export function UnitsRepresentation<P extends StructureProps>(visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
+    let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
+
+    let _props: P
+    let _structure: Structure
+    let _groups: ReadonlyArray<Unit.SymmetryGroup>
+
+    function create(structure: Structure, props: Partial<P> = {}) {
+        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
+        _props.colorTheme!.structure = structure
+
+        return Task.create('Creating StructureRepresentation', async ctx => {
+            if (!_structure) {
+                _groups = structure.unitSymmetryGroups;
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visual = visualCtor()
+                    await visual.create(ctx, group, _props)
+                    visuals.set(group.hashCode, { visual, group })
+                }
+            } else {
+                if (_structure.hashCode === structure.hashCode) {
+                    await update(_props)
+                } else {
+                    _groups = structure.unitSymmetryGroups;
+                    const newGroups: Unit.SymmetryGroup[] = []
+                    const oldUnitsVisuals = visuals
+                    visuals = new Map()
+                    for (let i = 0; i < _groups.length; i++) {
+                        const group = _groups[i];
+                        const visualGroup = oldUnitsVisuals.get(group.hashCode)
+                        if (visualGroup) {
+                            const { visual, group } = visualGroup
+                            if (!await visual.update(ctx, _props)) {
+                                await visual.create(ctx, group, _props)
+                            }
+                            oldUnitsVisuals.delete(group.hashCode)
+                        } else {
+                            newGroups.push(group)
+                            const visual = visualCtor()
+                            await visual.create(ctx, group, _props)
+                            visuals.set(group.hashCode, { visual, group })
+                        }
+                    }
+
+                    // for new groups, re-use leftover visuals
+                    const unusedVisuals: UnitsVisual<P>[] = []
+                    oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
+                    newGroups.forEach(async group => {
+                        const visual = unusedVisuals.pop() || visualCtor()
+                        await visual.create(ctx, group, _props)
+                        visuals.set(group.hashCode, { visual, group })
+                    })
+                    unusedVisuals.forEach(visual => visual.destroy())
+                }
+            }
+            _structure = structure
+        });
+    }
+
+    function update(props: Partial<P>) {
+        return Task.create('Updating StructureRepresentation', async ctx => {
+            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
+            _props.colorTheme!.structure = _structure
+
+            visuals.forEach(async ({ visual, group }) => {
+                if (!await visual.update(ctx, _props)) {
+                    await visual.create(ctx, group, _props)
+                }
+            })
+        })
+    }
+
+    function getLoci(pickingId: PickingId) {
+        let loci: Loci = EmptyLoci
+        visuals.forEach(({ visual }) => {
+            const _loci = visual.getLoci(pickingId)
+            if (!isEmptyLoci(_loci)) loci = _loci
+        })
+        return loci
+    }
+
+    function mark(loci: Loci, action: MarkerAction) {
+        visuals.forEach(({ visual }) => visual.mark(loci, action))
+    }
+
+    function destroy() {
+        visuals.forEach(({ visual }) => visual.destroy())
+        visuals.clear()
+    }
+
+    return {
+        get renderObjects() {
+            const renderObjects: RenderObject[] = []
+            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
+            return renderObjects
+        },
+        get props() {
+            return _props
+        },
+        create,
+        update,
+        getLoci,
+        mark,
+        destroy
+    }
+}

+ 127 - 0
src/mol-geo/representation/structure/units-visual.ts

@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit } from 'mol-model/structure';
+import { RepresentationProps, Visual } from '..';
+import { DefaultStructureMeshProps, MeshUpdateState } from '.';
+import { RuntimeContext } from 'mol-task';
+import { PickingId } from '../../util/picking';
+import { LocationIterator } from './visual/util/location-iterator';
+import { Mesh } from '../../shape/mesh';
+import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
+import { Loci, isEveryLoci } from 'mol-model/loci';
+import { MeshRenderObject } from 'mol-gl/render-object';
+import { createUnitsMeshRenderObject, createColors } from './visual/util/common';
+import { deepEqual, ValueCell } from 'mol-util';
+import { updateMeshValues, updateRenderableState } from '../util';
+import { Interval } from 'mol-data/int';
+
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+
+export const DefaultUnitsMeshProps = {
+    ...DefaultStructureMeshProps,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type UnitsMeshProps = typeof DefaultUnitsMeshProps
+
+export interface UnitsMeshVisualBuilder<P extends UnitsMeshProps> {
+    defaultProps: P
+    createMesh(ctx: RuntimeContext, unit: Unit, props: P, mesh?: Mesh): Promise<Mesh>
+    createLocationIterator(group: Unit.SymmetryGroup): LocationIterator
+    getLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number): Loci
+    mark(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean): boolean
+    setUpdateState(state: MeshUpdateState, newProps: P, currentProps: P): void
+}
+
+export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisualBuilder<P>): UnitsVisual<P> {
+    const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
+    const updateState = MeshUpdateState.create()
+
+    let renderObject: MeshRenderObject
+    let currentProps: P
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+    let locationIt: LocationIterator
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
+            currentProps = Object.assign({}, defaultProps, props)
+            currentGroup = group
+
+            const unit = group.units[0]
+            mesh = currentProps.unitKinds.includes(unit.kind)
+                ? await createMesh(ctx, unit, currentProps, mesh)
+                : Mesh.createEmpty(mesh)
+
+            locationIt = createLocationIterator(group)
+            renderObject = createUnitsMeshRenderObject(group, mesh, locationIt, currentProps)
+        },
+        async update(ctx: RuntimeContext, props: Partial<P>) {
+            const newProps = Object.assign({}, currentProps, props)
+            const unit = currentGroup.units[0]
+
+            if (!renderObject) return false
+
+            locationIt.reset()
+            MeshUpdateState.reset(updateState)
+            setUpdateState(updateState, newProps, currentProps)
+
+            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
+                updateState.createMesh = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateState.updateColor = true
+            }
+
+            //
+
+            if (updateState.createMesh) {
+                mesh = await createMesh(ctx, unit, newProps, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateState.updateColor = true
+            }
+
+            if (updateState.updateColor) {
+                createColors(locationIt, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getLoci(pickingId, currentGroup, renderObject.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            const { tMarker } = renderObject.values
+            const { elementCount, instanceCount } = locationIt
+
+            function apply(interval: Interval) {
+                const start = Interval.start(interval)
+                const end = Interval.end(interval)
+                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
+            }
+
+            let changed = false
+            if (isEveryLoci(loci)) {
+                apply(Interval.ofBounds(0, elementCount * instanceCount))
+                changed = true
+            } else {
+                changed = mark(loci, currentGroup, apply)
+            }
+            if (changed) {
+                ValueCell.update(tMarker, tMarker.ref.value)
+            }
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 138 - 0
src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts

@@ -0,0 +1,138 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, Structure, Link, StructureElement } from 'mol-model/structure';
+import { DefaultStructureProps, ComplexVisual, MeshUpdateState } from '..';
+import { RuntimeContext } from 'mol-task'
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { DefaultMeshProps } from '../../util';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { LocationIterator } from './util/location-iterator';
+import { createLinkCylinderMesh, DefaultLinkCylinderProps, LinkCylinderProps } from './util/link';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { ComplexMeshVisual } from '../complex-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { LinkType } from 'mol-model/structure/model/types';
+import { BitFlags } from 'mol-util';
+
+// TODO create seperate visual
+// for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) {
+//     const tl = carbohydrates.terminalLinks[i]
+//     const center = carbohydrates.elements[tl.carbohydrateIndex].geometry.center
+//     tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p)
+//     if (tl.fromCarbohydrate) {
+//         builder.addCylinder(center, p, 0.5, linkParams)
+//     } else {
+//         builder.addCylinder(p, center, 0.5, linkParams)
+//     }
+// }
+
+const radiusFactor = 0.3
+
+async function createCarbohydrateLinkCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
+    const { links, elements } = structure.carbohydrates
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const location = StructureElement.create()
+
+    const builderProps = {
+        linkCount: links.length,
+        referencePosition: (edgeIndex: number) => null,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const l = links[edgeIndex]
+            Vec3.copy(posA, elements[l.carbohydrateIndexA].geometry.center)
+            Vec3.copy(posB, elements[l.carbohydrateIndexB].geometry.center)
+        },
+        order: (edgeIndex: number) => 1,
+        flags: (edgeIndex: number) => BitFlags.create(LinkType.Flag.None),
+        radius: (edgeIndex: number) => {
+            const l = links[edgeIndex]
+            location.unit = elements[l.carbohydrateIndexA].unit
+            location.element = elements[l.carbohydrateIndexA].anomericCarbon
+            return sizeTheme.size(location) * radiusFactor
+        }
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const DefaultCarbohydrateLinkProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    ...DefaultLinkCylinderProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeThemeProps,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type CarbohydrateLinkProps = typeof DefaultCarbohydrateLinkProps
+
+export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkProps> {
+    return ComplexMeshVisual<CarbohydrateLinkProps>({
+        defaultProps: DefaultCarbohydrateLinkProps,
+        createMesh: createCarbohydrateLinkCylinderMesh,
+        createLocationIterator: CarbohydrateLinkIterator,
+        getLoci: getLinkLoci,
+        mark: markLink,
+        setUpdateState: (state: MeshUpdateState, newProps: CarbohydrateLinkProps, currentProps: CarbohydrateLinkProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
+    })
+}
+
+function CarbohydrateLinkIterator(structure: Structure): LocationIterator {
+    const { elements, links } = structure.carbohydrates
+    const elementCount = links.length
+    const instanceCount = 1
+    const location = Link.Location()
+    const getLocation = (elementIndex: number, instanceIndex: number) => {
+        const link = links[elementIndex]
+        const carbA = elements[link.carbohydrateIndexA]
+        const carbB = elements[link.carbohydrateIndexB]
+        const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon)
+        location.aUnit = carbA.unit
+        location.aIndex = indexA as StructureElement.UnitIndex
+        location.bUnit = carbB.unit
+        location.bIndex = indexB as StructureElement.UnitIndex
+        return location
+    }
+    return LocationIterator(elementCount, instanceCount, getLocation)
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, elementId } = pickingId
+    if (id === objectId) {
+        const { links, elements } = structure.carbohydrates
+        const l = links[elementId]
+        const carbA = elements[l.carbohydrateIndexA]
+        const carbB = elements[l.carbohydrateIndexB]
+        const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon)
+        return Link.Loci([
+            Link.Location(
+                carbA.unit, indexA as StructureElement.UnitIndex,
+                carbB.unit, indexB as StructureElement.UnitIndex
+            )
+        ])
+    }
+    return EmptyLoci
+}
+
+function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    const { getLinkIndex } = structure.carbohydrates
+
+    let changed = false
+    if (Link.isLoci(loci)) {
+        for (const l of loci.links) {
+            const idx = getLinkIndex(l.aUnit, l.aUnit.elements[l.aIndex], l.bUnit, l.bUnit.elements[l.bIndex])
+            if (idx !== undefined) {
+                if (apply(Interval.ofSingleton(idx))) changed = true
+            }
+        }
+    }
+    return changed
+}

+ 97 - 124
src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts

@@ -4,119 +4,113 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, Structure } from 'mol-model/structure';
-import { DefaultStructureProps, StructureVisual } from '..';
+import { Unit, Structure, StructureElement } from 'mol-model/structure';
+import { ComplexVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createIdentityTransform } from './util/common';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
-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, Mat4 } from 'mol-math/linear-algebra';
-import { createUniformColor } from '../../../util/color-data';
 import { getSaccharideShape, SaccharideShapes } from 'mol-model/structure/structure/carbohydrates/constants';
+import { LocationIterator } from './util/location-iterator';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { ComplexMeshVisual, DefaultComplexMeshProps } from '../complex-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
-const p = Vec3.zero()
 const pd = Vec3.zero()
 
-async function createCarbohydrateSymbolMesh(ctx: RuntimeContext, structure: Structure, mesh?: Mesh) {
+const sideFactor = 1.75 * 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
+const radiusFactor = 1.75
+
+async function createCarbohydrateSymbolMesh(ctx: RuntimeContext, structure: Structure, props: CarbohydrateSymbolProps, mesh?: Mesh) {
     const builder = MeshBuilder.create(256, 128, mesh)
 
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const { detail } = props
+
     const carbohydrates = structure.carbohydrates
+    const l = StructureElement.create()
 
-    function centerAlign(center: Vec3, normal: Vec3, direction: Vec3) {
+    for (let i = 0, il = carbohydrates.elements.length; i < il; ++i) {
+        const c = carbohydrates.elements[i];
+        const shapeType = getSaccharideShape(c.component.type)
+
+        l.unit = c.unit
+        l.element = c.unit.elements[c.anomericCarbon]
+        const size = sizeTheme.size(l)
+        const radius = size * radiusFactor
+        const side = size * sideFactor
+
+        const { center, normal, direction } = c.geometry
         Vec3.add(pd, center, direction)
         Mat4.targetTo(t, center, pd, normal)
         Mat4.setTranslation(t, center)
-    }
 
-    const side = 1.75 * 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
-    const radius = 1.75
-
-    const linkParams = { radiusTop: 0.4, radiusBottom: 0.4 }
-
-    for (let i = 0, il = carbohydrates.elements.length; i < il; ++i) {
-        const c = carbohydrates.elements[i];
-        if (!c.hasRing) continue;
-
-        const cGeo = c.geometry!
-        const shapeType = getSaccharideShape(c.component.type)
+        builder.setId(i * 2)
 
         switch (shapeType) {
             case SaccharideShapes.FilledSphere:
-                builder.addSphere(cGeo.center, radius, 2)
+                builder.addSphere(center, radius, detail)
                 break;
             case SaccharideShapes.FilledCube:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.scaleUniformly(t, t, side)
                 builder.addBox(t)
                 break;
             case SaccharideShapes.CrossedCube:
-                // TODO split
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.scaleUniformly(t, t, side)
-                builder.addBox(t)
+                builder.addPerforatedBox(t)
+                Mat4.mul(t, t, Mat4.rotZ90X180)
+                builder.setId(i * 2 + 1)
+                builder.addPerforatedBox(t)
                 break;
             case SaccharideShapes.FilledCone:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.scaleUniformly(t, t, side * 1.2)
                 builder.addOctagonalPyramid(t)
                 break
             case SaccharideShapes.DevidedCone:
-                // TODO split
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.scaleUniformly(t, t, side * 1.2)
-                builder.addOctagonalPyramid(t)
+                builder.addPerforatedOctagonalPyramid(t)
+                Mat4.mul(t, t, Mat4.rotZ90)
+                builder.setId(i * 2 + 1)
+                builder.addPerforatedOctagonalPyramid(t)
                 break
             case SaccharideShapes.FlatBox:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
                 builder.addBox(t)
                 break
             case SaccharideShapes.FilledStar:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 builder.addStar(t, { outerRadius: side, innerRadius: side / 2, thickness: side / 2, pointCount: 5 })
                 break
             case SaccharideShapes.FilledDiamond:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
                 builder.addOctahedron(t)
                 break
             case SaccharideShapes.DividedDiamond:
-                // TODO split
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
-                builder.addOctahedron(t)
+                builder.addPerforatedOctahedron(t)
+                Mat4.mul(t, t, Mat4.rotY90)
+                builder.setId(i * 2 + 1)
+                builder.addPerforatedOctahedron(t)
                 break
             case SaccharideShapes.FlatDiamond:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side / 2, side / 2))
                 builder.addDiamondPrism(t)
                 break
             case SaccharideShapes.Pentagon:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
                 builder.addPentagonalPrism(t)
                 break
             case SaccharideShapes.FlatHexagon:
             default:
-                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
                 Mat4.mul(t, t, Mat4.rotZYZ90)
                 Mat4.scale(t, t, Vec3.set(sVec, side / 1.5, side , side / 2))
                 builder.addHexagonalPrism(t)
@@ -124,91 +118,70 @@ async function createCarbohydrateSymbolMesh(ctx: RuntimeContext, structure: Stru
         }
     }
 
-    for (let i = 0, il = carbohydrates.links.length; i < il; ++i) {
-        const l = carbohydrates.links[i]
-        const centerA = carbohydrates.elements[l.carbohydrateIndexA].geometry!.center
-        const centerB = carbohydrates.elements[l.carbohydrateIndexB].geometry!.center
-        builder.addCylinder(centerA, centerB, 0.5, linkParams)
-    }
-
-    for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) {
-        const tl = carbohydrates.terminalLinks[i]
-        const center = carbohydrates.elements[tl.carbohydrateIndex].geometry!.center
-        tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p)
-        if (tl.fromCarbohydrate) {
-            builder.addCylinder(center, p, 0.5, linkParams)
-        } else {
-            builder.addCylinder(p, center, 0.5, linkParams)
-        }
-    }
-
     return builder.getMesh()
 }
 
 export const DefaultCarbohydrateSymbolProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    ...DefaultComplexMeshProps,
+    sizeTheme: { name: 'uniform', value: 1, factor: 1 } as SizeThemeProps,
     detail: 0,
     unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
 }
-export type CarbohydrateSymbolProps = Partial<typeof DefaultCarbohydrateSymbolProps>
-
-export function CarbohydrateSymbolVisual(): StructureVisual<CarbohydrateSymbolProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultCarbohydrateSymbolProps
-    let mesh: Mesh
-    let currentStructure: Structure
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, structure: Structure, props: CarbohydrateSymbolProps = {}) {
-            currentProps = Object.assign({}, DefaultCarbohydrateSymbolProps, props)
-            currentStructure = structure
-
-            const instanceCount = 1
-            const elementCount = currentStructure.elementCount
-
-            mesh = await createCarbohydrateSymbolMesh(ctx, currentStructure, mesh)
-            // console.log(mesh)
-
-            const transforms = createIdentityTransform()
-            const color = createUniformColor({ value: 0x999911 }) // TODO
-            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),
-                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
-            }
-            const state = createRenderableState(currentProps)
-
-            renderObject = createMeshRenderObject(values, state)
-        },
-        async update(ctx: RuntimeContext, props: CarbohydrateSymbolProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            if (!renderObject) return false
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            return false
-        },
-        getLoci(pickingId: PickingId) {
-            return EmptyLoci
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            // TODO
-        },
-        destroy() {
-            // TODO
-        }
+export type CarbohydrateSymbolProps = typeof DefaultCarbohydrateSymbolProps
+
+export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolProps> {
+    return ComplexMeshVisual<CarbohydrateSymbolProps>({
+        defaultProps: DefaultCarbohydrateSymbolProps,
+        createMesh: createCarbohydrateSymbolMesh,
+        createLocationIterator: CarbohydrateElementIterator,
+        getLoci: getCarbohydrateLoci,
+        mark: markCarbohydrate,
+        setUpdateState: () => {}
+    })
+}
+
+function CarbohydrateElementIterator(structure: Structure): LocationIterator {
+    const carbElements = structure.carbohydrates.elements
+    const elementCount = carbElements.length * 2
+    const instanceCount = 1
+    const location = StructureElement.create()
+    function getLocation (elementIndex: number, instanceIndex: number) {
+        const carb = carbElements[Math.floor(elementIndex / 2)]
+        location.unit = carb.unit
+        location.element = carb.anomericCarbon
+        return location
+    }
+    function isSecondary (elementIndex: number, instanceIndex: number) {
+        return (elementIndex % 2) === 1
+    }
+    return LocationIterator(elementCount, instanceCount, getLocation, isSecondary)
+}
+
+function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, elementId } = pickingId
+    if (id === objectId) {
+        const carb = structure.carbohydrates.elements[Math.floor(elementId / 2)]
+        const { unit } = carb
+        const index = OrderedSet.findPredecessorIndex(unit.elements, carb.anomericCarbon)
+        const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex)
+        return StructureElement.Loci([{ unit, indices }])
     }
+    return EmptyLoci
 }
+
+function markCarbohydrate(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
+    const { getElementIndex } = structure.carbohydrates
+
+    let changed = false
+    if (StructureElement.isLoci(loci)) {
+        for (const e of loci.elements) {
+            OrderedSet.forEach(e.indices, index => {
+                const idx = getElementIndex(e.unit, e.unit.elements[index])
+                if (idx !== undefined) {
+                    if (apply(Interval.ofBounds(idx * 2, idx * 2 + 2))) changed = true
+                }
+            })
+        }
+    }
+    return changed
+}

+ 58 - 102
src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts

@@ -4,116 +4,87 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Link, Structure } from 'mol-model/structure';
-import { DefaultStructureProps, StructureVisual } from '..';
+import { Link, Structure, StructureElement } from 'mol-model/structure';
+import { ComplexVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { createUniformColor } from '../../../util/color-data';
-import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
-import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
-import { SizeTheme } from '../../../theme';
-import { createIdentityTransform } from './util/common';
-import { updateMeshValues, updateRenderableState, createMeshValues, createRenderableState } from '../../util';
-// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { ComplexMeshVisual, DefaultComplexMeshProps } from '../complex-visual';
+import { LocationIterator } from './util/location-iterator';
+import { Interval } from 'mol-data/int';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { BitFlags } from 'mol-util';
+import { LinkType } from 'mol-model/structure/model/types';
 
 async function createCrossLinkRestraintCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
 
     const crossLinks = structure.crossLinkRestraints
     if (!crossLinks.count) return Mesh.createEmpty(mesh)
 
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const location = StructureElement.create()
+
     const builderProps = {
         linkCount: crossLinks.count,
         referencePosition: (edgeIndex: number) => null,
         position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
             const b = crossLinks.pairs[edgeIndex]
-            // console.log(b)
             const uA = b.unitA, uB = b.unitB
             uA.conformation.position(uA.elements[b.indexA], posA)
             uB.conformation.position(uB.elements[b.indexB], posB)
-            // console.log(posA, posB)
         },
         order: (edgeIndex: number) => 1,
-        flags: (edgeIndex: number) => 0
+        flags: (edgeIndex: number) => BitFlags.create(LinkType.Flag.None),
+        radius: (edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            location.unit = b.unitA
+            location.element = b.unitA.elements[b.indexA]
+            return sizeTheme.size(location)
+        }
     }
 
     return createLinkCylinderMesh(ctx, builderProps, props, mesh)
 }
 
 export const DefaultCrossLinkRestraintProps = {
-    ...DefaultStructureProps,
+    ...DefaultComplexMeshProps,
     ...DefaultLinkCylinderProps,
-    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
     flipSided: false,
     flatShaded: false,
 }
-export type CrossLinkRestraintProps = Partial<typeof DefaultCrossLinkRestraintProps>
-
-export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultCrossLinkRestraintProps
-    let mesh: Mesh
-    let currentStructure: Structure
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, structure: Structure, props: CrossLinkRestraintProps = {}) {
-            currentProps = Object.assign({}, DefaultCrossLinkRestraintProps, props)
-            currentStructure = structure
-
-            const elementCount = structure.crossLinkRestraints.count
-            const instanceCount = 1
-
-            mesh = await createCrossLinkRestraintCylinderMesh(ctx, structure, currentProps)
-
-            const transforms = createIdentityTransform()
-            const color = createUniformColor({ value: 0x119911 }) // TODO
-            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: CrossLinkRestraintProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            if (!renderObject) return false
-
-            // TODO create in-place
-            if (currentProps.radialSegments !== newProps.radialSegments) return false
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            return false
-        },
-        getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentStructure, renderObject.id)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentStructure, renderObject.values)
-        },
-        destroy() {
-            // TODO
+export type CrossLinkRestraintProps = typeof DefaultCrossLinkRestraintProps
+
+export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintProps> {
+    return ComplexMeshVisual<CrossLinkRestraintProps>({
+        defaultProps: DefaultCrossLinkRestraintProps,
+        createMesh: createCrossLinkRestraintCylinderMesh,
+        createLocationIterator: CrossLinkRestraintIterator,
+        getLoci: getLinkLoci,
+        mark: markLink,
+        setUpdateState: (state: MeshUpdateState, newProps: CrossLinkRestraintProps, currentProps: CrossLinkRestraintProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
         }
+    })
+}
+
+function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
+    const { pairs } = structure.crossLinkRestraints
+    const elementCount = pairs.length
+    const instanceCount = 1
+    const location = Link.Location()
+    const getLocation = (elementIndex: number, instanceIndex: number) => {
+        const pair = pairs[elementIndex]
+        location.aUnit = pair.unitA
+        location.aIndex = pair.indexA
+        location.bUnit = pair.unitB
+        location.bIndex = pair.indexB
+        return location
     }
+    return LocationIterator(elementCount, instanceCount, getLocation)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
@@ -121,45 +92,30 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     if (id === objectId) {
         const pair = structure.crossLinkRestraints.pairs[elementId]
         if (pair) {
-            return Link.Loci([{
-                aUnit: pair.unitA,
-                aIndex: pair.indexA,
-                bUnit: pair.unitB,
-                bIndex: pair.indexB
-            }])
+            return Link.Loci([
+                Link.Location(
+                    pair.unitA, pair.indexA as StructureElement.UnitIndex,
+                    pair.unitB, pair.indexB as StructureElement.UnitIndex
+                )
+            ])
         }
     }
     return EmptyLoci
 }
 
-function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) {
-    const tMarker = values.tMarker
-
+function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     const crossLinks = structure.crossLinkRestraints
-    const elementCount = crossLinks.count
-    const instanceCount = 1
 
     let changed = false
-    const array = tMarker.ref.value.array
-    if (isEveryLoci(loci)) {
-        applyMarkerAction(array, 0, elementCount * instanceCount, action)
-        changed = true
-    } else if (Link.isLoci(loci)) {
+    if (Link.isLoci(loci)) {
         for (const b of loci.links) {
             const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
             if (indices) {
                 for (let i = 0, il = indices.length; i < il; ++i) {
-                    const idx = indices[i]
-                    if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
-                        changed = true
-                    }
+                    if (apply(Interval.ofSingleton(indices[i]))) changed = true
                 }
             }
         }
-    } else {
-        return
-    }
-    if (changed) {
-        ValueCell.update(tMarker, tMarker.ref.value)
     }
+    return changed
 }

+ 10 - 14
src/mol-geo/representation/structure/visual/element-point.ts

@@ -11,9 +11,7 @@ import { Unit } from 'mol-model/structure';
 import { RuntimeContext } from 'mol-task'
 
 import { UnitsVisual, DefaultStructureProps } from '..';
-import VertexMap from '../../../shape/vertex-map';
-import { SizeTheme } from '../../../theme';
-import { markElement, getElementLoci } from './util/element';
+import { getElementLoci } from './util/element';
 import { createTransforms, createColors, createSizes } from './util/common';
 import { deepEqual, defaults } from 'mol-util';
 import { SortedArray } from 'mol-data/int';
@@ -23,10 +21,12 @@ import { Loci } from 'mol-model/loci';
 import { MarkerAction, createMarkers } from '../../../util/marker-data';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { fillSerial } from 'mol-util/array';
+import { StructureElementIterator } from './util/location-iterator';
+import { SizeThemeProps } from 'mol-view/theme/size';
 
 export const DefaultPointProps = {
     ...DefaultStructureProps,
-    sizeTheme: { name: 'physical' } as SizeTheme
+    sizeTheme: { name: 'physical' } as SizeThemeProps
 }
 export type PointProps = Partial<typeof DefaultPointProps>
 
@@ -69,17 +69,12 @@ export default function PointVisual(): UnitsVisual<PointProps> {
             const elementCount = _elements.length
             const instanceCount = group.units.length
 
-            const vertexMap = VertexMap.create(
-                elementCount,
-                elementCount + 1,
-                fillSerial(new Uint32Array(elementCount)),
-                fillSerial(new Uint32Array(elementCount + 1))
-            )
+            const locationIt = StructureElementIterator.fromGroup(group)
 
             const vertices = createPointVertices(_units[0])
             const transforms = createTransforms(group)
-            const color = createColors(group, elementCount, colorTheme)
-            const size = createSizes(group, vertexMap, sizeTheme)
+            const color = createColors(locationIt, colorTheme)
+            const size = createSizes(locationIt, sizeTheme)
             const marker = createMarkers(instanceCount * elementCount)
 
             const values: PointValues = {
@@ -129,10 +124,11 @@ export default function PointVisual(): UnitsVisual<PointProps> {
             return false
         },
         getLoci(pickingId: PickingId) {
-            return getElementLoci(renderObject.id, currentGroup, pickingId)
+            return getElementLoci(pickingId, currentGroup, renderObject.id)
         },
         mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+            // TODO
+            // markElement(loci, action, currentGroup, renderObject.values)
         },
         destroy() {
             // TODO

+ 17 - 103
src/mol-geo/representation/structure/visual/element-sphere.ts

@@ -5,112 +5,26 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
-import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './util/common';
-import { createElementSphereMesh, markElement, getElementRadius, getElementLoci } from './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 { createMarkers, MarkerAction } from '../../../util/marker-data';
-import { Loci } from 'mol-model/loci';
-import { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { UnitsVisual, MeshUpdateState } from '..';
+import { createElementSphereMesh, markElement, getElementLoci } from './util/element';
+import { StructureElementIterator } from './util/location-iterator';
+import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 
 export const DefaultElementSphereProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps,
+    detail: 0
 }
-export type ElementSphereProps = Partial<typeof DefaultElementSphereProps>
+export type ElementSphereProps = typeof DefaultElementSphereProps
 
 export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultElementSphereProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: ElementSphereProps = {}) {
-            currentProps = Object.assign({}, DefaultElementSphereProps, props)
-            currentGroup = group
-
-            const { detail, colorTheme, sizeTheme, unitKinds } = { ...DefaultElementSphereProps, ...props }
-            const instanceCount = group.units.length
-            const elementCount = group.elements.length
-            const unit = group.units[0]
-
-            const radius = getElementRadius(unit, sizeTheme)
-            mesh = unitKinds.includes(unit.kind)
-                ? await createElementSphereMesh(ctx, unit, radius, detail, 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: ElementSphereProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            if (!renderObject) return false
-
-            let updateColor = false
-
-            if (newProps.detail !== currentProps.detail) {
-                const unit = currentGroup.units[0]
-                const radius = getElementRadius(unit, newProps.sizeTheme)
-                mesh = await createElementSphereMesh(ctx, unit, radius, newProps.detail, 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 sphere colors');
-                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
-            }
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            currentProps = newProps
-            return true
-        },
-        getLoci(pickingId: PickingId) {
-            return getElementLoci(renderObject.id, currentGroup, pickingId)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
+    return UnitsMeshVisual<ElementSphereProps>({
+        defaultProps: DefaultElementSphereProps,
+        createMesh: createElementSphereMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: (state: MeshUpdateState, newProps: ElementSphereProps, currentProps: ElementSphereProps) => {
+            state.createMesh = newProps.detail !== currentProps.detail
         }
-    }
-}
+    })
+}

+ 44 - 105
src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts

@@ -4,25 +4,19 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Link, Structure } from 'mol-model/structure';
-import { DefaultStructureProps, StructureVisual } from '..';
+import { Link, Structure, StructureElement } from 'mol-model/structure';
+import { ComplexVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { createUniformColor } from '../../../util/color-data';
-import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
-import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
-import { SizeTheme } from '../../../theme';
-import { createIdentityTransform } from './util/common';
-import { updateMeshValues, updateRenderableState, createMeshValues, createRenderableState } from '../../util';
-// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { LinkIterator } from './util/location-iterator';
+import { ComplexMeshVisual, DefaultComplexMeshProps } from '../complex-visual';
+import { Interval } from 'mol-data/int';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { BitFlags } from 'mol-util';
 
 async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
     const links = structure.links
@@ -30,6 +24,9 @@ async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: S
 
     if (!bondCount) return Mesh.createEmpty(mesh)
 
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const location = StructureElement.create()
+
     const builderProps = {
         linkCount: bondCount,
         referencePosition: (edgeIndex: number) => null, // TODO
@@ -40,119 +37,61 @@ async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: S
             uB.conformation.position(uB.elements[b.indexB], posB)
         },
         order: (edgeIndex: number) => bonds[edgeIndex].order,
-        flags: (edgeIndex: number) => bonds[edgeIndex].flag
+        flags: (edgeIndex: number) => BitFlags.create(bonds[edgeIndex].flag),
+        radius: (edgeIndex: number) => {
+            const b = bonds[edgeIndex]
+            location.unit = b.unitA
+            location.element = b.unitA.elements[b.indexA]
+            return sizeTheme.size(location)
+        }
     }
 
     return createLinkCylinderMesh(ctx, builderProps, props, mesh)
 }
 
 export const DefaultInterUnitLinkProps = {
-    ...DefaultStructureProps,
+    ...DefaultComplexMeshProps,
     ...DefaultLinkCylinderProps,
-    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
-export type InterUnitLinkProps = Partial<typeof DefaultInterUnitLinkProps>
-
-export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultInterUnitLinkProps
-    let mesh: Mesh
-    let currentStructure: Structure
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, structure: Structure, props: InterUnitLinkProps = {}) {
-            currentProps = Object.assign({}, DefaultInterUnitLinkProps, props)
-            currentStructure = structure
-
-            const elementCount = structure.links.bondCount
-            const instanceCount = 1
-
-            mesh = await createInterUnitLinkCylinderMesh(ctx, structure, currentProps)
-
-            const transforms = createIdentityTransform()
-            const color = createUniformColor({ value: 0x999911 }) // TODO
-            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: InterUnitLinkProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            if (!renderObject) return false
-
-            // TODO create in-place
-            if (currentProps.radialSegments !== newProps.radialSegments) return false
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            return false
-        },
-        getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentStructure, renderObject.id)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentStructure, renderObject.values)
-        },
-        destroy() {
-            // TODO
+export type InterUnitLinkProps = typeof DefaultInterUnitLinkProps
+
+export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkProps> {
+    return ComplexMeshVisual<InterUnitLinkProps>({
+        defaultProps: DefaultInterUnitLinkProps,
+        createMesh: createInterUnitLinkCylinderMesh,
+        createLocationIterator: LinkIterator.fromStructure,
+        getLoci: getLinkLoci,
+        mark: markLink,
+        setUpdateState: (state: MeshUpdateState, newProps: InterUnitLinkProps, currentProps: InterUnitLinkProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
         }
-    }
+    })
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
     const { objectId, elementId } = pickingId
     if (id === objectId) {
         const bond = structure.links.bonds[elementId]
-        return Link.Loci([{
-            aUnit: bond.unitA,
-            aIndex: bond.indexA,
-            bUnit: bond.unitB,
-            bIndex: bond.indexB
-        }])
+        return Link.Loci([
+            Link.Location(
+                bond.unitA, bond.indexA as StructureElement.UnitIndex,
+                bond.unitB, bond.indexB as StructureElement.UnitIndex
+            )
+        ])
     }
     return EmptyLoci
 }
 
-function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) {
-    const tMarker = values.tMarker
-
-    const links = structure.links
-    const elementCount = links.bondCount
-    const instanceCount = 1
-
+function markLink(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean) {
     let changed = false
-    const array = tMarker.ref.value.array
-    if (isEveryLoci(loci)) {
-        applyMarkerAction(array, 0, elementCount * instanceCount, action)
-        changed = true
-    } else if (Link.isLoci(loci)) {
+    if (Link.isLoci(loci)) {
         for (const b of loci.links) {
-            const _idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
-            if (_idx !== -1) {
-                const idx = _idx
-                if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
-                    changed = true
-                }
+            const idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
+            if (idx !== -1) {
+                if (apply(Interval.ofSingleton(idx))) changed = true
             }
         }
-    } else {
-        return
-    }
-    if (changed) {
-        ValueCell.update(tMarker, tMarker.ref.value)
     }
+    return changed
 }

+ 39 - 104
src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts

@@ -5,29 +5,26 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, Link } from 'mol-model/structure';
-import { UnitsVisual, DefaultStructureProps } from '..';
+import { Unit, Link, StructureElement } from 'mol-model/structure';
+import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
 import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh } from './util/link';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
 import { Mesh } from '../../../shape/mesh';
 import { PickingId } from '../../../util/picking';
 import { Vec3 } from 'mol-math/linear-algebra';
-// import { createUniformColor } from '../../../util/color-data';
-import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
-import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
-import { SizeTheme } from '../../../theme';
-import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
-import { createTransforms } from './util/common';
-import { createMeshValues, createRenderableState, updateMeshValues, updateRenderableState } from '../../util';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { LinkIterator } from './util/location-iterator';
+import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
+import { Interval } from 'mol-data/int';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { BitFlags } from 'mol-util';
 
 async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit, props: LinkCylinderProps, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
 
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const location = StructureElement.create(unit)
+
     const elements = unit.elements;
     const links = unit.links
     const { edgeCount, a, b, edgeProps, offset } = links
@@ -56,124 +53,62 @@ async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit,
             pos(elements[b[edgeIndex]], posB)
         },
         order: (edgeIndex: number) => _order[edgeIndex],
-        flags: (edgeIndex: number) => _flags[edgeIndex]
+        flags: (edgeIndex: number) => BitFlags.create(_flags[edgeIndex]),
+        radius: (edgeIndex: number) => {
+            location.element = elements[a[edgeIndex]]
+            return sizeTheme.size(location)
+        }
     }
 
     return createLinkCylinderMesh(ctx, builderProps, props, mesh)
 }
 
 export const DefaultIntraUnitLinkProps = {
-    ...DefaultStructureProps,
+    ...DefaultUnitsMeshProps,
     ...DefaultLinkCylinderProps,
-    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
-export type IntraUnitLinkProps = Partial<typeof DefaultIntraUnitLinkProps>
+export type IntraUnitLinkProps = typeof DefaultIntraUnitLinkProps
 
 export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultIntraUnitLinkProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: IntraUnitLinkProps = {}) {
-            currentProps = Object.assign({}, DefaultIntraUnitLinkProps, props)
-            currentGroup = group
-
-            const unit = group.units[0]
-            const elementCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
-            const instanceCount = group.units.length
-
-            mesh = await createIntraUnitLinkCylinderMesh(ctx, unit, currentProps)
-
-            const transforms = createTransforms(group)
-            const color = chainIdLinkColorData({ group, elementCount }) // TODO
-            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: IntraUnitLinkProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            if (!renderObject) return false
-
-            // TODO create in-place
-            if (currentProps.radialSegments !== newProps.radialSegments) return false
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            return true
-        },
-        getLoci(pickingId: PickingId) {
-            return getLinkLoci(pickingId, currentGroup, renderObject.id)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markLink(loci, action, currentGroup, renderObject.values)
-        },
-        destroy() {
-            // TODO
-        }
-    }
+    return UnitsMeshVisual<IntraUnitLinkProps>({
+        defaultProps: DefaultIntraUnitLinkProps,
+        createMesh: createIntraUnitLinkCylinderMesh,
+        createLocationIterator: LinkIterator.fromGroup,
+        getLoci: getLinkLoci,
+        mark: markLink,
+        setUpdateState: () => {}
+    })
 }
 
 function getLinkLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
     const { objectId, instanceId, elementId } = pickingId
     const unit = group.units[instanceId]
     if (id === objectId && Unit.isAtomic(unit)) {
-        return Link.Loci([{
-            aUnit: unit,
-            aIndex: unit.links.a[elementId],
-            bUnit: unit,
-            bIndex: unit.links.b[elementId]
-        }])
+        return Link.Loci([
+            Link.Location(
+                unit, unit.links.a[elementId] as StructureElement.UnitIndex,
+                unit, unit.links.b[elementId] as StructureElement.UnitIndex
+            )
+        ])
     }
     return EmptyLoci
 }
 
-function markLink(loci: Loci, action: MarkerAction, group: Unit.SymmetryGroup, values: MarkerData) {
-    const tMarker = values.tMarker
+function markLink(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
     const unit = group.units[0]
-    if (!Unit.isAtomic(unit)) return
-
-    const elementCount = unit.links.edgeCount * 2
-    const instanceCount = group.units.length
 
     let changed = false
-    const array = tMarker.ref.value.array
-    if (isEveryLoci(loci)) {
-        applyMarkerAction(array, 0, elementCount * instanceCount, action)
-        changed = true
-    } else if (Link.isLoci(loci)) {
+    if (Unit.isAtomic(unit) && Link.isLoci(loci)) {
         for (const b of loci.links) {
             const unitIdx = Unit.findUnitById(b.aUnit.id, group.units)
             if (unitIdx !== -1) {
-                const _idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
-                if (_idx !== -1) {
-                    const idx = _idx
-                    if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
-                        changed = true
-                    }
+                const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
+                if (idx !== -1) {
+                    if (apply(Interval.ofSingleton(idx))) changed = true
                 }
             }
         }
-    } else {
-        return
-    }
-    if (changed) {
-        ValueCell.update(tMarker, tMarker.ref.value)
     }
+    return changed
 }

+ 16 - 102
src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts

@@ -4,28 +4,18 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
 import { Unit } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
+import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './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 { createMarkers, MarkerAction } from '../../../util/marker-data';
-import { Loci } from 'mol-model/loci';
-import { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 import { MeshBuilder } from '../../../shape/mesh-builder';
 import { getElementLoci, markElement } from './util/element';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { Segmentation, SortedArray } from 'mol-data/int';
 import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
 import { getElementIndexForAtomId, getElementIndexForAtomRole } from 'mol-model/structure/util';
+import { StructureElementIterator } from './util/location-iterator';
+import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
 
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
@@ -40,7 +30,7 @@ const center = Vec3.zero()
 const t = Mat4.identity()
 const sVec = Vec3.zero()
 
-async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, props: {}, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
 
     const builder = MeshBuilder.create(256, 128, mesh)
@@ -104,7 +94,7 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?:
             }
 
             if (i % 10000 === 0 && ctx.shouldUpdate) {
-                await ctx.update({ message: 'Gap mesh', current: i });
+                await ctx.update({ message: 'Nucleotide block mesh', current: i });
             }
             ++i
         }
@@ -114,93 +104,17 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?:
 }
 
 export const DefaultNucleotideBlockProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps
 }
-export type NucleotideBlockProps = Partial<typeof DefaultNucleotideBlockProps>
+export type NucleotideBlockProps = typeof DefaultNucleotideBlockProps
 
 export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultNucleotideBlockProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: NucleotideBlockProps = {}) {
-            currentProps = Object.assign({}, DefaultNucleotideBlockProps, props)
-            currentGroup = group
-
-            const { colorTheme, unitKinds } = { ...DefaultNucleotideBlockProps, ...props }
-            const instanceCount = group.units.length
-            const elementCount = group.elements.length
-            const unit = group.units[0]
-
-            mesh = unitKinds.includes(unit.kind)
-                ? await createNucleotideBlockMesh(ctx, unit, mesh)
-                : Mesh.createEmpty(mesh)
-            // console.log(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),
-                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
-            }
-            const state = createRenderableState(currentProps)
-
-            renderObject = createMeshRenderObject(values, state)
-        },
-        async update(ctx: RuntimeContext, props: NucleotideBlockProps) {
-            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 createNucleotideBlockMesh(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 nucleotide block colors');
-                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
-            }
-
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
-
-            currentProps = newProps
-            return true
-        },
-        getLoci(pickingId: PickingId) {
-            return getElementLoci(renderObject.id, currentGroup, pickingId)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
-        }
-    }
-}
+    return UnitsMeshVisual<NucleotideBlockProps>({
+        defaultProps: DefaultNucleotideBlockProps,
+        createMesh: createNucleotideBlockMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: () => {}
+    })
+}

+ 43 - 110
src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts

@@ -4,51 +4,59 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
+import { Unit, StructureElement } from 'mol-model/structure';
+import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './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 { createMarkers, MarkerAction } from '../../../util/marker-data';
-import { Loci } from 'mol-model/loci';
-import { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 import { MeshBuilder } from '../../../shape/mesh-builder';
 import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer';
 import { getElementLoci, markElement } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { StructureElementIterator } from './util/location-iterator';
+import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { CylinderProps } from '../../../primitive/cylinder';
+
+export interface PolymerBackboneCylinderProps {
+    sizeTheme: SizeThemeProps
+    radialSegments: number
+}
 
-async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerBackboneCylinderProps, mesh?: Mesh) {
     const polymerElementCount = getPolymerElementCount(unit)
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
-    console.log('polymerElementCount backbone', polymerElementCount)
 
-    // TODO better vertex count estimates
-    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const { radialSegments } = props
+
+    const vertexCountEstimate = radialSegments * 2 * polymerElementCount * 2
+    const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
 
     const { elements } = unit
     const pos = unit.conformation.invariantPosition
     const pA = Vec3.zero()
     const pB = Vec3.zero()
+    const l = StructureElement.create(unit)
+    const cylinderProps: CylinderProps = { radiusTop: 1, radiusBottom: 1, radialSegments }
 
     let i = 0
     const polymerBackboneIt = PolymerBackboneIterator(unit)
     while (polymerBackboneIt.hasNext) {
-        // TODO size theme
         const { centerA, centerB } = polymerBackboneIt.move()
-        pos(elements[centerA.element], pA)
-        pos(elements[centerB.element], pB)
+        const elmA = elements[centerA.element]
+        const elmB = elements[centerB.element]
+        pos(elmA, pA)
+        pos(elmB, pB)
+
+        l.element = elmA
+        cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
         builder.setId(centerA.element)
-        builder.addCylinder(pA, pB, 0.5, { radiusTop: 0.2, radiusBottom: 0.2 })
+        builder.addCylinder(pA, pB, 0.5, cylinderProps)
+
+        l.element = elmB
+        cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
         builder.setId(centerB.element)
-        builder.addCylinder(pB, pA, 0.5, { radiusTop: 0.2, radiusBottom: 0.2 })
+        builder.addCylinder(pB, pA, 0.5, cylinderProps)
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
             await ctx.update({ message: 'Backbone mesh', current: i, max: polymerElementCount });
@@ -60,93 +68,18 @@ async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit
 }
 
 export const DefaultPolymerBackboneProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps,
+    radialSegments: 16
 }
-export type PolymerBackboneProps = Partial<typeof DefaultPolymerBackboneProps>
+export type PolymerBackboneProps = 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)
-            // console.log(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),
-                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
-            }
-            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) {
-            return getElementLoci(renderObject.id, currentGroup, pickingId)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
-        }
-    }
-}
+    return UnitsMeshVisual<PolymerBackboneProps>({
+        defaultProps: DefaultPolymerBackboneProps,
+        createMesh: createPolymerBackboneCylinderMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: () => {}
+    })
+}

+ 39 - 123
src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts

@@ -4,28 +4,18 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, StructureElement } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
+import { Unit } from 'mol-model/structure';
+import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './util/common';
-import { markElement } from './util/element';
-import { deepEqual } from 'mol-util';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
+import { markElement, getElementLoci } from './util/element';
 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 { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 import { MeshBuilder } from '../../../shape/mesh-builder';
 import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
+import { StructureElementIterator } from './util/location-iterator';
+import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
 
 const t = Mat4.identity()
 const sVec = Vec3.zero()
@@ -33,13 +23,22 @@ const n0 = Vec3.zero()
 const n1 = Vec3.zero()
 const upVec = Vec3.zero()
 
-async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+const depthFactor = 4
+const widthFactor = 4
+const heightFactor = 6
+
+export interface PolymerDirectionWedgeProps {
+    sizeTheme: SizeThemeProps
+}
+
+async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, props: PolymerDirectionWedgeProps, mesh?: Mesh) {
     const polymerElementCount = getPolymerElementCount(unit)
-    console.log('polymerElementCount direction', polymerElementCount)
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
-    // TODO better vertex count estimates
-    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+    const sizeTheme = SizeTheme(props.sizeTheme)
+
+    const vertexCount = polymerElementCount * 24
+    const builder = MeshBuilder.create(vertexCount, vertexCount / 10, mesh)
     const linearSegments = 1
 
     const state = createCurveSegmentState(linearSegments)
@@ -58,21 +57,18 @@ async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit,
         interpolateCurveSegment(state, v, tension)
 
         if ((isSheet && !v.secStrucChange) || !isSheet) {
+            const size = sizeTheme.size(v.center)
+            const depth = depthFactor * size
+            const width = widthFactor * size
+            const height = heightFactor * size
 
-            let width = 0.5, height = 1.2, depth = 0.6
-            if (isNucleic) {
-                Vec3.fromArray(n0, binormalVectors, 0)
-                Vec3.fromArray(n1, binormalVectors, 3)
-                Vec3.normalize(upVec, Vec3.add(upVec, n0, n1))
-                depth = 0.9
-            } else {
-                Vec3.fromArray(n0, normalVectors, 0)
-                Vec3.fromArray(n1, normalVectors, 3)
-                Vec3.normalize(upVec, Vec3.add(upVec, n0, n1))
-            }
+            const vectors = isNucleic ? binormalVectors : normalVectors
+            Vec3.fromArray(n0, vectors, 0)
+            Vec3.fromArray(n1, vectors, 3)
+            Vec3.normalize(upVec, Vec3.add(upVec, n0, n1))
 
             Mat4.targetTo(t, v.p3, v.p1, upVec)
-            Mat4.mul(t, t, Mat4.rotY90)
+            Mat4.mul(t, t, Mat4.rotY90Z180)
             Mat4.scale(t, t, Vec3.set(sVec, height, width, depth))
             Mat4.setTranslation(t, v.p2)
             builder.addWedge(t)
@@ -88,97 +84,17 @@ async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit,
 }
 
 export const DefaultPolymerDirectionProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps
 }
-export type PolymerDirectionProps = Partial<typeof DefaultPolymerDirectionProps>
+export type PolymerDirectionProps = typeof DefaultPolymerDirectionProps
 
 export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultPolymerDirectionProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerDirectionProps = {}) {
-            currentProps = Object.assign({}, DefaultPolymerDirectionProps, props)
-            currentGroup = group
-
-            const { colorTheme, unitKinds } = { ...DefaultPolymerDirectionProps, ...props }
-            const instanceCount = group.units.length
-            const elementCount = group.elements.length
-            const unit = group.units[0]
-
-            mesh = unitKinds.includes(unit.kind)
-                ? await createPolymerDirectionWedgeMesh(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: PolymerDirectionProps) {
-            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 createPolymerDirectionWedgeMesh(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 direction 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 StructureElement.UnitIndex);
-                return StructureElement.Loci([{ unit, indices }])
-            }
-            return EmptyLoci
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
-        }
-    }
-}
+    return UnitsMeshVisual<PolymerDirectionProps>({
+        defaultProps: DefaultPolymerDirectionProps,
+        createMesh: createPolymerDirectionWedgeMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: () => {}
+    })
+}

+ 47 - 110
src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts

@@ -4,56 +4,68 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
+import { Unit, StructureElement } from 'mol-model/structure';
+import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './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 { createMarkers, MarkerAction } from '../../../util/marker-data';
-import { Loci } from 'mol-model/loci';
-import { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 import { MeshBuilder } from '../../../shape/mesh-builder';
 import { getPolymerGapCount, PolymerGapIterator } from './util/polymer';
 import { getElementLoci, markElement } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { StructureElementIterator } from './util/location-iterator';
+import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { CylinderProps } from '../../../primitive/cylinder';
+
+const segmentCount = 10
+
+export interface PolymerGapCylinderProps {
+    sizeTheme: SizeThemeProps
+    radialSegments: number
+}
 
-async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerGapCylinderProps, mesh?: Mesh) {
     const polymerGapCount = getPolymerGapCount(unit)
     if (!polymerGapCount) return Mesh.createEmpty(mesh)
-    console.log('polymerGapCount', polymerGapCount)
 
-    // TODO better vertex count estimates
-    const builder = MeshBuilder.create(polymerGapCount * 30, polymerGapCount * 30 / 2, mesh)
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const { radialSegments } = props
+
+    const vertexCountEstimate = segmentCount * radialSegments * 2 * polymerGapCount * 2
+    const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
 
     const { elements } = unit
     const pos = unit.conformation.invariantPosition
     const pA = Vec3.zero()
     const pB = Vec3.zero()
+    const l = StructureElement.create(unit)
+    const cylinderProps: CylinderProps = {
+        radiusTop: 1, radiusBottom: 1, topCap: true, bottomCap: true, radialSegments
+    }
 
     let i = 0
     const polymerGapIt = PolymerGapIterator(unit)
     while (polymerGapIt.hasNext) {
-        // TODO size theme
         const { centerA, centerB } = polymerGapIt.move()
         if (centerA.element === centerB.element) {
             builder.setId(centerA.element)
             pos(elements[centerA.element], pA)
             builder.addSphere(pA, 0.6, 0)
         } else {
-            pos(elements[centerA.element], pA)
-            pos(elements[centerB.element], pB)
+            const elmA = elements[centerA.element]
+            const elmB = elements[centerB.element]
+            pos(elmA, pA)
+            pos(elmB, pB)
+
+            l.element = elmA
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
             builder.setId(centerA.element)
-            builder.addFixedCountDashedCylinder(pA, pB, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+            builder.addFixedCountDashedCylinder(pA, pB, 0.5, segmentCount, cylinderProps)
+
+            l.element = elmB
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
             builder.setId(centerB.element)
-            builder.addFixedCountDashedCylinder(pB, pA, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+            builder.addFixedCountDashedCylinder(pB, pA, 0.5, segmentCount, cylinderProps)
         }
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
@@ -66,93 +78,18 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mes
 }
 
 export const DefaultPolymerGapProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps,
+    radialSegments: 16
 }
-export type PolymerGapProps = Partial<typeof DefaultPolymerGapProps>
+export type PolymerGapProps = typeof DefaultPolymerGapProps
 
 export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> {
-    let renderObject: MeshRenderObject
-    let currentProps: typeof DefaultPolymerGapProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerGapProps = {}) {
-            currentProps = Object.assign({}, DefaultPolymerGapProps, props)
-            currentGroup = group
-
-            const { colorTheme, unitKinds } = { ...DefaultPolymerGapProps, ...props }
-            const instanceCount = group.units.length
-            const elementCount = group.elements.length
-            const unit = group.units[0]
-
-            mesh = unitKinds.includes(unit.kind)
-                ? await createPolymerGapCylinderMesh(ctx, unit, mesh)
-                : Mesh.createEmpty(mesh)
-            // console.log(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),
-                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
-            }
-            const state = createRenderableState(currentProps)
-
-            renderObject = createMeshRenderObject(values, state)
-        },
-        async update(ctx: RuntimeContext, props: PolymerGapProps) {
-            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 createPolymerGapCylinderMesh(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) {
-            return getElementLoci(renderObject.id, currentGroup, pickingId)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
-        }
-    }
-}
+    return UnitsMeshVisual<PolymerGapProps>({
+        defaultProps: DefaultPolymerGapProps,
+        createMesh: createPolymerGapCylinderMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: () => {}
+    })
+}

+ 50 - 119
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -4,39 +4,37 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, StructureElement } from 'mol-model/structure';
-import { DefaultStructureProps, UnitsVisual } from '..';
+import { Unit } from 'mol-model/structure';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
-import { createTransforms, createColors } from './util/common';
-import { markElement } from './util/element';
-import { deepEqual } from 'mol-util';
-import { MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../../util/mesh-data';
+import { markElement, getElementLoci } from './util/element';
 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 { SizeTheme } from '../../../theme';
-import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
 import { MeshBuilder } from '../../../shape/mesh-builder';
 import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
 import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
+import { StructureElementIterator } from './util/location-iterator';
+import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+
+export interface PolymerTraceMeshProps {
+    sizeTheme: SizeThemeProps
+    linearSegments: number
+    radialSegments: number
+    aspectRatio: number
+    arrowFactor: number
+}
 
 // TODO handle polymer ends properly
 
-async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: PolymerTraceMeshProps, mesh?: Mesh) {
     const polymerElementCount = getPolymerElementCount(unit)
-    console.log('polymerElementCount trace', polymerElementCount)
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
-    // TODO better vertex count estimates
-    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
-    const linearSegments = 8
-    const radialSegments = 12
+    const sizeTheme = SizeTheme(props.sizeTheme)
+    const { linearSegments, radialSegments, aspectRatio, arrowFactor } = props
+
+    const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2
+    const builder = MeshBuilder.create(vertexCount, vertexCount / 10, mesh)
 
     const state = createCurveSegmentState(linearSegments)
     const { curvePoints, normalVectors, binormalVectors } = state
@@ -52,21 +50,23 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Me
         const isHelix = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix)
         const tension = (isNucleic || isSheet) ? 0.5 : 0.9
 
-        // console.log('ELEMENT', i)
         interpolateCurveSegment(state, v, tension)
 
-        let width = 0.2, height = 0.2
+        let width = sizeTheme.size(v.center)
 
-        // TODO size theme
         if (isSheet) {
-            width = 0.15; height = 1.0
-            const arrowHeight = v.secStrucChange ? 1.7 : 0
+            const height = width * aspectRatio
+            const arrowHeight = v.secStrucChange ? height * arrowFactor : 0
             builder.addSheet(curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true)
         } else {
+            let height: number
             if (isHelix) {
-                width = 0.2; height = 1.0
+                height = width * aspectRatio
             } else if (isNucleic) {
-                width = 1.5; height = 0.3
+                height = width * aspectRatio;
+                [width, height] = [height, width]
+            } else {
+                height = width
             }
             builder.addTube(curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true)
         }
@@ -81,97 +81,28 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Me
 }
 
 export const DefaultPolymerTraceProps = {
-    ...DefaultMeshProps,
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
-    detail: 0,
-    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+    ...DefaultUnitsMeshProps,
+    linearSegments: 8,
+    radialSegments: 12,
+    aspectRatio: 5,
+    arrowFactor: 1.5
 }
-export type PolymerTraceProps = Partial<typeof DefaultPolymerTraceProps>
+export type PolymerTraceProps = 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 StructureElement.UnitIndex);
-                return StructureElement.Loci([{ unit, indices }])
-            }
-            return EmptyLoci
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            markElement(renderObject.values.tMarker, currentGroup, loci, action)
-        },
-        destroy() {
-            // TODO
+    return UnitsMeshVisual<PolymerTraceProps>({
+        defaultProps: DefaultPolymerTraceProps,
+        createMesh: createPolymerTraceMesh,
+        createLocationIterator: StructureElementIterator.fromGroup,
+        getLoci: getElementLoci,
+        mark: markElement,
+        setUpdateState: (state: MeshUpdateState, newProps: PolymerTraceProps, currentProps: PolymerTraceProps) => {
+            state.createMesh = (
+                newProps.linearSegments !== currentProps.linearSegments ||
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.aspectRatio !== currentProps.aspectRatio ||
+                newProps.arrowFactor !== currentProps.arrowFactor
+            )
         }
-    }
-}
+    })
+}

+ 73 - 37
src/mol-geo/representation/structure/visual/util/common.ts

@@ -5,16 +5,22 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Unit } from 'mol-model/structure';
+import { Unit, Structure } from 'mol-model/structure';
 import { Mat4 } from 'mol-math/linear-algebra'
 
-import { createUniformColor, ColorData } from '../../../../util/color-data';
-import { createUniformSize, SizeData } from '../../../../util/size-data';
-import { physicalSizeData } from '../../../../theme/structure/size/physical';
-import VertexMap from '../../../../shape/vertex-map';
-import { ColorTheme, SizeTheme } from '../../../../theme';
-import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdElementColorData } from '../../../../theme/structure/color';
-import { ValueCell, defaults } from 'mol-util';
+import { createUniformColor, ColorData, createElementColor, createElementInstanceColor, createInstanceColor } from '../../../../util/color-data';
+import { createUniformSize, SizeData, createElementSize, createElementInstanceSize, createInstanceSize } from '../../../../util/size-data';
+import { ValueCell } from 'mol-util';
+import { LocationIterator } from './location-iterator';
+import { Mesh } from '../../../../shape/mesh';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../../util/mesh-data';
+import { MeshProps, createMeshValues, createRenderableState } from '../../../util';
+import { StructureProps } from '../..';
+import { createMarkers } from '../../../../util/marker-data';
+import { createMeshRenderObject } from 'mol-gl/render-object';
+import { ColorThemeProps, ColorTheme } from 'mol-view/theme/color';
+import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
 
 export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
     const unitCount = units.length
@@ -32,38 +38,68 @@ export function createIdentityTransform(transforms?: ValueCell<Float32Array>) {
     return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform)
 }
 
-export function createColors(group: Unit.SymmetryGroup, elementCount: number, props: ColorTheme, colorData?: ColorData) {
-    switch (props.name) {
-        case 'atom-index':
-            return elementIndexColorData({ group, elementCount }, colorData)
-        case 'chain-id':
-            return chainIdElementColorData({ group, elementCount }, colorData)
-        case 'element-symbol':
-            return elementSymbolColorData({ group, elementCount }, colorData)
-        case 'instance-index':
-            return instanceIndexColorData({ group, elementCount }, colorData)
-        case 'uniform':
-            return createUniformColor(props, colorData)
+export function createColors(locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData) {
+    const colorTheme = ColorTheme(props)
+    switch (colorTheme.kind) {
+        case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData)
+        case 'element': return createElementColor(locationIt, colorTheme.color, colorData)
+        case 'elementInstance': return createElementInstanceColor(locationIt, colorTheme.color, colorData)
+        case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData)
     }
 }
 
-// export function createLinkColors(group: Unit.SymmetryGroup, props: ColorTheme, colorData?: ColorData): ColorData {
-//     switch (props.name) {
-//         case 'atom-index':
-//         case 'chain-id':
-//         case 'element-symbol':
-//         case 'instance-index':
-//             return chainIdLinkColorData({ group, vertexMap }, colorData)
-//         case 'uniform':
-//             return createUniformColor(props, colorData)
-//     }
-// }
+export function createSizes(locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): SizeData {
+    const sizeTheme = SizeTheme(props)
+    switch (sizeTheme.kind) {
+        case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData)
+        case 'element': return createElementSize(locationIt, sizeTheme.size, sizeData)
+        case 'elementInstance': return createElementInstanceSize(locationIt, sizeTheme.size, sizeData)
+        case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData)
+    }
+}
+
+type StructureMeshProps = Required<MeshProps & StructureProps>
+
+function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+    const { instanceCount, elementCount } = locationIt
+    const color = createColors(locationIt, props.colorTheme)
+    const marker = createMarkers(instanceCount * elementCount)
 
-export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: SizeTheme): SizeData {
-    switch (props.name) {
-        case 'uniform':
-            return createUniformSize(props)
-        case 'physical':
-            return physicalSizeData(defaults(props.factor, 1), { group, vertexMap })
+    const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+    return {
+        ...getMeshData(mesh),
+        ...color,
+        ...marker,
+        aTransform: transforms,
+        elements: mesh.indexBuffer,
+        ...createMeshValues(props, counts)
     }
+}
+
+export function createComplexMeshValues(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+    const transforms = createIdentityTransform()
+    return _createMeshValues(transforms, mesh, locationIt, props)
+}
+
+export function createUnitsMeshValues(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+    const transforms = createTransforms(group)
+    return _createMeshValues(transforms, mesh, locationIt, props)
+}
+
+export function createComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = createComplexMeshValues(structure, mesh, locationIt, props)
+    const state = createRenderableState(props)
+    return createMeshRenderObject(values, state)
+}
+
+export function createUnitsMeshRenderObject(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = createUnitsMeshValues(group, mesh, locationIt, props)
+    const state = createRenderableState(props)
+    return createMeshRenderObject(values, state)
+}
+
+export function updateComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+    const transforms = createIdentityTransform()
+    return _createMeshValues(transforms, mesh, locationIt, props)
 }

+ 20 - 39
src/mol-geo/representation/structure/visual/util/element.ts

@@ -6,32 +6,25 @@
 
 import { Vec3 } from 'mol-math/linear-algebra';
 import { Unit, StructureElement } from 'mol-model/structure';
-import { SizeTheme } from '../../../../theme';
 import { RuntimeContext } from 'mol-task';
 import { sphereVertexCount } from '../../../../primitive/sphere';
 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, EmptyLoci } from 'mol-model/loci';
-import { MarkerAction, applyMarkerAction } from '../../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
 import { Interval, OrderedSet } from 'mol-data/int';
-import { getPhysicalRadius } from '../../../../theme/structure/size/physical';
 import { PickingId } from '../../../../util/picking';
+import { SizeTheme, SizeThemeProps } from 'mol-view/theme/size';
 
-export function getElementRadius(unit: Unit, props: SizeTheme): StructureElement.Property<number> {
-    switch (props.name) {
-        case 'uniform':
-            return () => props.value
-        case 'physical':
-            const radius = getPhysicalRadius(unit)
-            const factor = defaults(props.factor, 1)
-            return (l) => radius(l) * factor
-    }
+export interface ElementSphereMeshProps {
+    sizeTheme: SizeThemeProps,
+    detail: number,
 }
 
-export async function createElementSphereMesh(ctx: RuntimeContext, unit: Unit, radius: StructureElement.Property<number>, detail: number, mesh?: Mesh) {
+export async function createElementSphereMesh(ctx: RuntimeContext, unit: Unit, props: ElementSphereMeshProps, mesh?: Mesh) {
+    const { detail } = props
+
     const { elements } = unit;
+    const sizeTheme = SizeTheme(props.sizeTheme)
     const elementCount = elements.length;
     const vertexCount = elementCount * sphereVertexCount(detail)
     const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
@@ -46,7 +39,7 @@ export async function createElementSphereMesh(ctx: RuntimeContext, unit: Unit, r
         pos(elements[i], v)
 
         meshBuilder.setId(i)
-        meshBuilder.addSphere(v, radius(l), detail)
+        meshBuilder.addSphere(v, sizeTheme.size(l), detail)
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
             await ctx.update({ message: 'Sphere mesh', current: i, max: elementCount });
@@ -56,43 +49,31 @@ 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
+export function markElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
     const elementCount = group.elements.length
-    const instanceCount = group.units.length
-    const array = tMarker.ref.value.array
-    if (isEveryLoci(loci)) {
-        applyMarkerAction(array, 0, elementCount * instanceCount, action)
-        changed = true
-    } else if (StructureElement.isLoci(loci)) {
+
+    let changed = false
+    if (StructureElement.isLoci(loci)) {
         for (const e of loci.elements) {
             const unitIdx = Unit.findUnitById(e.unit.id, group.units)
             if (unitIdx !== -1) {
                 if (Interval.is(e.indices)) {
-                    const idxStart = unitIdx * elementCount + Interval.start(e.indices);
-                    const idxEnd = unitIdx * elementCount + Interval.end(e.indices);
-                    if (applyMarkerAction(array, idxStart, idxEnd, action) && !changed) {
-                        changed = true
-                    }
+                    const start = unitIdx * elementCount + Interval.start(e.indices);
+                    const end = unitIdx * elementCount + Interval.end(e.indices);
+                    if (apply(Interval.ofBounds(start, end))) changed = true
                 } else {
                     for (let i = 0, _i = e.indices.length; i < _i; i++) {
                         const idx = unitIdx * elementCount + e.indices[i];
-                        if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
-                            changed = true
-                        }
+                        if (apply(Interval.ofSingleton(idx))) changed = true
                     }
                 }
             }
         }
-    } else {
-        return
-    }
-    if (changed) {
-        ValueCell.update(tMarker, tMarker.ref.value)
     }
+    return changed
 }
 
-export function getElementLoci(id: number, group: Unit.SymmetryGroup, pickingId: PickingId) {
+export function getElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
     const { objectId, instanceId, elementId } = pickingId
     if (id === objectId) {
         const unit = group.units[instanceId]

+ 20 - 23
src/mol-geo/representation/structure/visual/util/link.ts

@@ -10,9 +10,12 @@ import { Mesh } from '../../../../shape/mesh';
 import { MeshBuilder } from '../../../../shape/mesh-builder';
 import { LinkType } from 'mol-model/structure/model/types';
 import { DefaultMeshProps } from '../../../util';
+import { SizeThemeProps } from 'mol-view/theme/size';
+import { CylinderProps } from '../../../../primitive/cylinder';
 
 export const DefaultLinkCylinderProps = {
     ...DefaultMeshProps,
+    sizeTheme: { name: 'uniform', value: 0.15 } as SizeThemeProps,
     linkScale: 0.4,
     linkSpacing: 1,
     linkRadius: 0.25,
@@ -55,7 +58,8 @@ export interface LinkCylinderMeshBuilderProps {
     referencePosition(edgeIndex: number): Vec3 | null
     position(posA: Vec3, posB: Vec3, edgeIndex: number): void
     order(edgeIndex: number): number
-    flags(edgeIndex: number): LinkType.Flag
+    flags(edgeIndex: number): LinkType
+    radius(edgeIndex: number): number
 }
 
 /**
@@ -63,39 +67,32 @@ export interface LinkCylinderMeshBuilderProps {
  * the half closer to the first vertex, i.e. vertex a.
  */
 export async function createLinkCylinderMesh(ctx: RuntimeContext, linkBuilder: LinkCylinderMeshBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
-    const { linkCount, referencePosition, position, order, flags } = linkBuilder
+    const { linkCount, referencePosition, position, order, flags, radius } = linkBuilder
 
     if (!linkCount) return Mesh.createEmpty(mesh)
 
-    // approximate vertextCount (* 2), exact calculation would need to take
-    // multiple cylinders for bond orders and metall coordinations into account
-    const vertexCount = props.radialSegments * 2 * linkCount * 2
-    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
+    const { linkScale, linkSpacing, radialSegments } = props
+
+    const vertexCountEstimate = radialSegments * 2 * linkCount * 2
+    const meshBuilder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 4, mesh)
 
     const va = Vec3.zero()
     const vb = Vec3.zero()
     const vShift = Vec3.zero()
-
-    const { linkScale, linkSpacing, linkRadius, radialSegments } = props
-
-    const cylinderParams = {
-        height: 1,
-        radiusTop: linkRadius,
-        radiusBottom: linkRadius,
-        radialSegments
-    }
+    const cylinderProps: CylinderProps = { radiusTop: 1, radiusBottom: 1, radialSegments }
 
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         position(va, vb, edgeIndex)
 
+        const linkRadius = radius(edgeIndex)
         const o = order(edgeIndex)
-        const f = flags(edgeIndex) as any as LinkType // TODO
+        const f = flags(edgeIndex)
         meshBuilder.setId(edgeIndex)
 
         if (LinkType.is(f, LinkType.Flag.MetallicCoordination)) {
             // show metall coordinations with dashed cylinders
-            cylinderParams.radiusTop = cylinderParams.radiusBottom = linkRadius / 3
-            meshBuilder.addFixedCountDashedCylinder(va, vb, 0.5, 7, cylinderParams)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3
+            meshBuilder.addFixedCountDashedCylinder(va, vb, 0.5, 7, cylinderProps)
         } else if (o === 2 || o === 3) {
             // show bonds with order 2 or 3 using 2 or 3 parallel cylinders
             const multiRadius = linkRadius * (linkScale / (0.5 * o))
@@ -104,13 +101,13 @@ export async function createLinkCylinderMesh(ctx: RuntimeContext, linkBuilder: L
             calculateShiftDir(vShift, va, vb, referencePosition(edgeIndex))
             Vec3.setMagnitude(vShift, vShift, absOffset)
 
-            cylinderParams.radiusTop = cylinderParams.radiusBottom = multiRadius
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius
 
-            if (o === 3) meshBuilder.addCylinder(va, vb, 0.5, cylinderParams)
-            meshBuilder.addDoubleCylinder(va, vb, 0.5, vShift, cylinderParams)
+            if (o === 3) meshBuilder.addCylinder(va, vb, 0.5, cylinderProps)
+            meshBuilder.addDoubleCylinder(va, vb, 0.5, vShift, cylinderProps)
         } else {
-            cylinderParams.radiusTop = cylinderParams.radiusBottom = linkRadius
-            meshBuilder.addCylinder(va, vb, 0.5, cylinderParams)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius
+            meshBuilder.addCylinder(va, vb, 0.5, cylinderProps)
         }
 
         if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) {

+ 141 - 0
src/mol-geo/representation/structure/visual/util/location-iterator.ts

@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Iterator } from 'mol-data';
+import { Unit, StructureElement, Structure, Link } from 'mol-model/structure';
+import { NullLocation, Location } from 'mol-model/location';
+
+export interface LocationValue {
+    location: Location
+    index: number
+    elementIndex: number
+    instanceIndex: number
+    isSecondary: boolean
+}
+
+export const NullLocationValue: LocationValue = {
+    location: NullLocation,
+    index: 0,
+    elementIndex: 0,
+    instanceIndex: 0,
+    isSecondary: false
+}
+
+export interface LocationIterator extends Iterator<LocationValue> {
+    readonly hasNext: boolean
+    readonly isNextNewInstance: boolean
+    readonly elementCount: number
+    readonly instanceCount: number
+    move(): LocationValue
+    reset(): void
+    skipInstance(): void
+}
+
+type LocationGetter = (elementIndex: number, instanceIndex: number) => Location
+type IsSecondaryGetter = (elementIndex: number, instanceIndex: number) => boolean
+
+export function LocationIterator(elementCount: number, instanceCount: number, getLocation: LocationGetter, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
+    const value: LocationValue = {
+        location: NullLocation as Location,
+        index: 0,
+        elementIndex: 0,
+        instanceIndex: 0,
+        isSecondary: false
+    }
+
+    let hasNext = value.elementIndex < elementCount
+    let isNextNewInstance = false
+    let elementIndex = 0
+    let instanceIndex = 0
+
+    return {
+        get hasNext () { return hasNext },
+        get isNextNewInstance () { return isNextNewInstance },
+        get elementCount () { return elementCount },
+        get instanceCount () { return instanceCount },
+        move() {
+            if (hasNext) {
+                value.elementIndex = elementIndex
+                value.instanceIndex = instanceIndex
+                value.index = instanceIndex * elementCount + elementIndex
+                value.location = getLocation(elementIndex, instanceIndex)
+                value.isSecondary = isSecondary(elementIndex, instanceIndex)
+                ++elementIndex
+                if (elementIndex === elementCount) {
+                    ++instanceIndex
+                    isNextNewInstance = true
+                    if (instanceIndex < instanceCount) elementIndex = 0
+                } else {
+                    isNextNewInstance = false
+                }
+                hasNext = elementIndex < elementCount
+            }
+            return value
+        },
+        reset() {
+            value.location = NullLocation
+            value.index = 0
+            value.elementIndex = 0
+            value.instanceIndex = 0
+            value.isSecondary = false
+
+            hasNext = value.elementIndex < elementCount
+            isNextNewInstance = false
+            elementIndex = 0
+            instanceIndex = 0
+        },
+        skipInstance() {
+            if (hasNext && value.instanceIndex === instanceIndex) {
+                ++instanceIndex
+                elementIndex = 0
+                hasNext = instanceIndex < instanceCount
+            }
+        }
+    }
+}
+
+export namespace StructureElementIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const unit = group.units[0]
+        const elementCount = group.elements.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create(unit)
+        const getLocation = (elementIndex: number, instanceIndex: number) => {
+            location.element = unit.elements[elementIndex]
+            return location
+        }
+        return LocationIterator(elementCount, instanceCount, getLocation)
+    }
+}
+
+export namespace LinkIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const unit = group.units[0]
+        const elementCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
+        const instanceCount = group.units.length
+        const location = StructureElement.create(unit)
+        const getLocation = (elementIndex: number, instanceIndex: number) => {
+            location.element = unit.elements[(unit as Unit.Atomic).links.a[elementIndex]]
+            return location
+        }
+        return LocationIterator(elementCount, instanceCount, getLocation)
+    }
+
+    export function fromStructure(structure: Structure): LocationIterator {
+        const elementCount = structure.links.bondCount
+        const instanceCount = 1
+        const location = Link.Location()
+        const getLocation = (elementIndex: number, instanceIndex: number) => {
+            const bond = structure.links.bonds[elementIndex]
+            location.aUnit = bond.unitA
+            location.aIndex = bond.indexA as StructureElement.UnitIndex
+            location.bUnit = bond.unitB
+            location.bIndex = bond.indexB as StructureElement.UnitIndex
+            return location
+        }
+        return LocationIterator(elementCount, instanceCount, getLocation)
+    }
+}

+ 2 - 2
src/mol-geo/representation/util.ts

@@ -18,7 +18,7 @@ export const DefaultBaseProps = {
     useFog: true,
     quality: 'auto' as VisualQuality
 }
-export type BaseProps = Partial<typeof DefaultBaseProps>
+export type BaseProps = typeof DefaultBaseProps
 
 export const DefaultMeshProps = {
     ...DefaultBaseProps,
@@ -26,7 +26,7 @@ export const DefaultMeshProps = {
     flipSided: false,
     flatShaded: false,
 }
-export type MeshProps = Partial<typeof DefaultMeshProps>
+export type MeshProps = typeof DefaultMeshProps
 
 type Counts = { drawCount: number, elementCount: number, instanceCount: number }
 

+ 10 - 4
src/mol-geo/representation/volume/index.ts

@@ -11,18 +11,24 @@ import { VolumeData } from 'mol-model/volume';
 import { PickingId } from '../../util/picking';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
+import { DefaultBaseProps } from '../util';
 
 export interface VolumeVisual<P extends RepresentationProps = {}> extends Visual<VolumeData, P> { }
 
 export interface VolumeRepresentation<P extends RepresentationProps = {}> extends Representation<VolumeData, P> { }
 
-export function VolumeRepresentation<P>(visualCtor: (volumeData: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
+export const DefaultVolumeProps = {
+    ...DefaultBaseProps
+}
+export type VolumeProps = typeof DefaultVolumeProps
+
+export function VolumeRepresentation<P extends VolumeProps>(visualCtor: (volumeData: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
     const renderObjects: RenderObject[] = []
     let _volumeData: VolumeData
     let _props: P
 
-    function create(volumeData: VolumeData, props: P = {} as P) {
-        _props = props
+    function create(volumeData: VolumeData, props: Partial<P> = {}) {
+        _props = Object.assign({}, DefaultVolumeProps, _props, props)
         return Task.create('VolumeRepresentation.create', async ctx => {
             _volumeData = volumeData
             const visual = visualCtor(_volumeData)
@@ -31,7 +37,7 @@ export function VolumeRepresentation<P>(visualCtor: (volumeData: VolumeData) =>
         });
     }
 
-    function update(props: P) {
+    function update(props: Partial<P>) {
         return Task.create('VolumeRepresentation.update', async ctx => {})
     }
 

+ 2 - 2
src/mol-geo/representation/volume/surface.ts

@@ -13,7 +13,7 @@ import { VolumeVisual } from '.';
 import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object';
 import { ValueCell, defaults } from 'mol-util';
 import { Mat4 } from 'mol-math/linear-algebra';
-import { createUniformColor } from '../../util/color-data';
+import { createValueColor } from '../../util/color-data';
 import { getMeshData } from '../../util/mesh-data';
 import { RenderableState, MeshValues } from 'mol-gl/renderable';
 import { PickingId } from '../../util/picking';
@@ -65,7 +65,7 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
             }
 
             const instanceCount = 1
-            const color = createUniformColor({ value: 0x7ec0ee })
+            const color = createValueColor(0x7ec0ee)
             const marker = createEmptyMarkers()
 
             const values: MeshValues = {

+ 20 - 3
src/mol-geo/shape/mesh-builder.ts

@@ -16,10 +16,12 @@ import { getNormalMatrix } from '../util';
 import { addSheet } from '../primitive/sheet';
 import { addTube } from '../primitive/tube';
 import { StarProps, Star } from '../primitive/star';
-import { Octahedron } from '../primitive/octahedron';
+import { Octahedron, PerforatedOctahedron } from '../primitive/octahedron';
 import { Primitive } from '../primitive/primitive';
-import { Wedge, Box, DiamondPrism, PentagonalPrism, HexagonalPrism } from '../primitive/prism';
-import { OctagonalPyramide } from '../primitive/pyramid';
+import { DiamondPrism, PentagonalPrism, HexagonalPrism } from '../primitive/prism';
+import { OctagonalPyramide, PerforatedOctagonalPyramide } from '../primitive/pyramid';
+import { PerforatedBox, Box } from '../primitive/box';
+import { Wedge } from '../primitive/wedge';
 
 export interface MeshBuilderState {
     vertices: ChunkedArray<number, 3>
@@ -30,14 +32,17 @@ export interface MeshBuilderState {
 export interface MeshBuilder {
     add(t: Mat4, _vertices: ArrayLike<number>, _normals: ArrayLike<number>, _indices?: ArrayLike<number>): void
     addBox(t: Mat4): void
+    addPerforatedBox(t: Mat4): void
     addPlane(t: Mat4, props?: PlaneProps): void
     addWedge(t: Mat4): void
     addDiamondPrism(t: Mat4): void
     addPentagonalPrism(t: Mat4): void
     addHexagonalPrism(t: Mat4): void
     addOctagonalPyramid(t: Mat4): void
+    addPerforatedOctagonalPyramid(t: Mat4): void
     addStar(t: Mat4, props?: StarProps): void
     addOctahedron(t: Mat4): void
+    addPerforatedOctahedron(t: Mat4): void
     addCylinder(start: Vec3, end: Vec3, lengthScale: number, props: CylinderProps): void
     addDoubleCylinder(start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps): void
     addFixedCountDashedCylinder(start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps): void
@@ -140,6 +145,10 @@ export namespace MeshBuilder {
                 const { vertices, normals, indices } = Box()
                 add(t, vertices, normals, indices)
             },
+            addPerforatedBox: (t: Mat4) => {
+                const { vertices, normals, indices } = PerforatedBox()
+                add(t, vertices, normals, indices)
+            },
             addPlane: (t: Mat4, props?: PlaneProps) => {
                 const { vertices, normals, indices } = Plane(props)
                 add(t, vertices, normals, indices)
@@ -164,6 +173,10 @@ export namespace MeshBuilder {
                 const { vertices, normals, indices } = OctagonalPyramide()
                 add(t, vertices, normals, indices)
             },
+            addPerforatedOctagonalPyramid: (t: Mat4) => {
+                const { vertices, normals, indices } = PerforatedOctagonalPyramide()
+                add(t, vertices, normals, indices)
+            },
             addStar: (t: Mat4, props?: StarProps) => {
                 const { vertices, normals, indices } = Star(props)
                 add(t, vertices, normals, indices)
@@ -172,6 +185,10 @@ export namespace MeshBuilder {
                 const { vertices, normals, indices } = Octahedron()
                 add(t, vertices, normals, indices)
             },
+            addPerforatedOctahedron: (t: Mat4) => {
+                const { vertices, normals, indices } = PerforatedOctahedron()
+                add(t, vertices, normals, indices)
+            },
             addCylinder: (start: Vec3, end: Vec3, lengthScale: number, props: CylinderProps) => {
                 const d = Vec3.distance(start, end) * lengthScale
                 props.height = d

+ 0 - 49
src/mol-geo/theme/index.ts

@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Color } from 'mol-util/color';
-// import { Loci } from 'mol-model/loci';
-// import { Structure } from 'mol-model/structure';
-
-export interface UniformColorTheme {
-    name: 'uniform'
-    value: Color
-}
-
-export interface ScaleColorTheme {
-    name:  'atom-index' | 'chain-id' | 'element-symbol' | 'instance-index'
-    domain?: [number, number]
-}
-
-// interface StructureColorProvider {
-//     uniform(): Color
-//     instance(instanceIdx: number): Color
-//     element(elementIdx: number): Color
-//     elementInstance(elementIdx: number, instanceIdx: number): Color
-
-//     lociColor(loci: Loci): Color
-// }
-
-// export namespace ColorProvider {
-//     export function fromLociColor(lociColor: (loci: Loci) => Color) {
-
-//         return
-//     }
-// }
-
-export type ColorTheme = UniformColorTheme | ScaleColorTheme
-
-export interface UniformSizeTheme {
-    name: 'uniform',
-    value: number
-}
-
-export interface ScaleSizeTheme {
-    name: 'physical' // van-der-Waals for atoms, given radius for coarse spheres
-    factor?: number // scaling factor
-}
-
-export type SizeTheme = UniformSizeTheme | ScaleSizeTheme

+ 0 - 71
src/mol-geo/theme/structure/color/chain-id.ts

@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Unit, StructureProperties, StructureElement } from 'mol-model/structure';
-
-import { StructureColorDataProps } from '.';
-import { ColorData, createElementColor, createUniformColor } from '../../../util/color-data';
-import { ColorScale } from 'mol-util/color';
-
-function getAsymId(unit: Unit): StructureElement.Property<string> {
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            return StructureProperties.chain.label_asym_id
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            return StructureProperties.coarse.asym_id
-    }
-    throw new Error('unhandled unit kind')
-}
-
-export function chainIdColorData(props: StructureColorDataProps, locationFn: (l: StructureElement, renderElementIdx: number) => void, colorData?: ColorData) {
-    const { group: { units }, elementCount } = props
-    const unit = units[0]
-
-    const map = unit.model.properties.asymIdSerialMap
-    const count = map.size
-
-    const domain = [ 0, count - 1 ]
-    const scale = ColorScale.create({ domain })
-    const asym_id = getAsymId(unit)
-
-    const l = StructureElement.create()
-    l.unit = unit
-
-    return createElementColor({
-        colorFn: (renderElementIdx: number) => {
-            locationFn(l, renderElementIdx)
-            return scale.color(map.get(asym_id(l)) || 0)
-        },
-        elementCount
-    }, colorData)
-}
-
-export function chainIdElementColorData(props: StructureColorDataProps, colorData?: ColorData) {
-    const elements = props.group.units[0].elements
-    function locationFn(l: StructureElement, renderElementIdx: number) {
-        l.element = elements[renderElementIdx]
-    }
-    return chainIdColorData(props, locationFn, colorData)
-}
-
-export function chainIdLinkColorData(props: StructureColorDataProps, colorData?: ColorData): ColorData {
-    const unit = props.group.units[0]
-    const elements = unit.elements
-    let locationFn: (l: StructureElement, renderElementIdx: number) => void
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const { a } = unit.links
-            locationFn = (l: StructureElement, renderElementIdx: number) => {
-                l.element = elements[a[renderElementIdx]]
-            }
-            return chainIdColorData(props, locationFn, colorData)
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            // no chainId link color for coarse units, return uniform grey color
-            return createUniformColor({ value: 0xCCCCCC }, colorData)
-    }
-}

+ 0 - 22
src/mol-geo/theme/structure/color/element-index.ts

@@ -1,22 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { ColorScale } from 'mol-util/color';
-import { StructureColorDataProps } from '.';
-import { createElementInstanceColor, ColorData } from '../../../util/color-data';
-
-export function elementIndexColorData(props: StructureColorDataProps, colorData?: ColorData) {
-    const { group: { units }, elementCount } = props
-    const instanceCount = units.length
-
-    const domain = [ 0, instanceCount * elementCount - 1 ]
-    const scale = ColorScale.create({ domain })
-    return createElementInstanceColor({
-        colorFn: (instanceIdx, elementIdx) => scale.color(instanceIdx * elementCount + elementIdx),
-        instanceCount,
-        elementCount
-    }, colorData)
-}

+ 0 - 17
src/mol-geo/theme/structure/color/index.ts

@@ -1,17 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Unit } from 'mol-model/structure';
-
-export interface StructureColorDataProps {
-    group: Unit.SymmetryGroup,
-    elementCount: number
-}
-
-export { elementIndexColorData } from './element-index'
-export { chainIdElementColorData } from './chain-id'
-export { elementSymbolColorData } from './element-symbol'
-export { instanceIndexColorData } from './instance-index'

+ 0 - 21
src/mol-geo/theme/structure/color/instance-index.ts

@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { ColorScale } from 'mol-util/color';
-import { StructureColorDataProps } from '.';
-import { createInstanceColor, ColorData } from '../../../util/color-data';
-
-export function instanceIndexColorData(props: StructureColorDataProps, colorData?: ColorData) {
-    const { group: { units } } = props
-    const instanceCount = units.length
-
-    const domain = [ 0, instanceCount - 1 ]
-    const scale = ColorScale.create({ domain })
-    return createInstanceColor({
-        colorFn: scale.color,
-        instanceCount
-    }, colorData)
-}

+ 0 - 15
src/mol-geo/theme/structure/size/index.ts

@@ -1,15 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Unit } from 'mol-model/structure';
-import VertexMap from '../../../shape/vertex-map';
-
-export interface StructureSizeDataProps {
-    group: Unit.SymmetryGroup,
-    vertexMap: VertexMap
-}
-
-export { physicalSizeData } from './physical'

+ 0 - 39
src/mol-geo/theme/structure/size/physical.ts

@@ -1,39 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { StructureElement, Unit, StructureProperties } from 'mol-model/structure';
-import { StructureSizeDataProps } from '.';
-import { createAttributeSize } from '../../../util/size-data';
-
-export function getPhysicalRadius(unit: Unit): StructureElement.Property<number> {
-    if (Unit.isAtomic(unit)) {
-        return StructureProperties.atom.vdw_radius
-    } else if (Unit.isSpheres(unit)) {
-        return StructureProperties.coarse.sphere_radius
-    } else {
-        return () => 0
-    }
-}
-
-/**
- * Create attribute data with the physical size of an element,
- * i.e. vdw for atoms and radius for coarse spheres
- */
-export function physicalSizeData(factor: number, props: StructureSizeDataProps) {
-    const { group, vertexMap } = props
-    const unit = group.units[0]
-    const elements = group.elements;
-    const radius = getPhysicalRadius(unit)
-    const l = StructureElement.create()
-    l.unit = unit
-    return createAttributeSize({
-        sizeFn: (elementIdx: number) => {
-            l.element = elements[elementIdx]
-            return radius(l) * factor
-        },
-        vertexMap
-    })
-}

+ 28 - 74
src/mol-geo/util/color-data.ts

@@ -7,10 +7,11 @@
 import { ValueCell } from 'mol-util';
 import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
 import { Color } from 'mol-util/color';
-import VertexMap from '../shape/vertex-map';
 import { Vec2, Vec3 } from 'mol-math/linear-algebra';
+import { LocationIterator } from '../representation/structure/visual/util/location-iterator';
+import { Location, NullLocation } from 'mol-model/location';
 
-export type ColorType = 'uniform' | 'attribute' | 'instance' | 'element' | 'elementInstance'
+export type ColorType = 'uniform' | 'instance' | 'element' | 'elementInstance'
 
 export type ColorData = {
     uColor: ValueCell<Vec3>,
@@ -20,6 +21,8 @@ export type ColorData = {
     dColorType: ValueCell<string>,
 }
 
+export type LocationColor = (location: Location, isSecondary: boolean) => Color
+
 const emptyColorTexture = { array: new Uint8Array(3), width: 1, height: 1 }
 function createEmptyColorTexture() {
     return {
@@ -28,21 +31,16 @@ function createEmptyColorTexture() {
     }
 }
 
-export interface UniformColorProps {
-    value: Color
-}
-
-/** Creates color uniform */
-export function createUniformColor(props: UniformColorProps, colorData?: ColorData): ColorData {
+export function createValueColor(value: Color, colorData?: ColorData): ColorData {
     if (colorData) {
-        ValueCell.update(colorData.uColor, Color.toRgbNormalized(props.value) as Vec3)
+        ValueCell.update(colorData.uColor, Color.toRgbNormalized(value) as Vec3)
         if (colorData.dColorType.ref.value !== 'uniform') {
             ValueCell.update(colorData.dColorType, 'uniform')
         }
         return colorData
     } else {
         return {
-            uColor: ValueCell.create(Color.toRgbNormalized(props.value) as Vec3),
+            uColor: ValueCell.create(Color.toRgbNormalized(value) as Vec3),
             aColor: ValueCell.create(new Float32Array(0)),
             ...createEmptyColorTexture(),
             dColorType: ValueCell.create('uniform'),
@@ -50,38 +48,9 @@ export function createUniformColor(props: UniformColorProps, colorData?: ColorDa
     }
 }
 
-export interface AttributeColorProps {
-    colorFn: (elementIdx: number) => Color
-    vertexMap: VertexMap
-}
-
-/** Creates color attribute with color for each element (i.e. shared across instances/units) */
-export function createAttributeColor(props: AttributeColorProps, colorData?: ColorData): ColorData {
-    const { colorFn, vertexMap } = props
-    const { idCount, offsetCount, offsets } = vertexMap
-    const colors = new Float32Array(idCount * 3);
-    for (let i = 0, il = offsetCount - 1; i < il; ++i) {
-        const start = offsets[i]
-        const end = offsets[i + 1]
-        const hexColor = colorFn(i)
-        for (let i = start, il = end; i < il; ++i) {
-            Color.toArrayNormalized(hexColor, colors, i * 3)
-        }
-    }
-    if (colorData) {
-        ValueCell.update(colorData.aColor, colors)
-        if (colorData.dColorType.ref.value !== 'attribute') {
-            ValueCell.update(colorData.dColorType, 'attribute')
-        }
-        return colorData
-    } else {
-        return {
-            uColor: ValueCell.create(Vec3.zero()),
-            aColor: ValueCell.create(colors),
-            ...createEmptyColorTexture(),
-            dColorType: ValueCell.create('attribute'),
-        }
-    }
+/** Creates color uniform */
+export function createUniformColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+    return createValueColor(colorFn(NullLocation, false), colorData)
 }
 
 export function createTextureColor(colors: TextureImage, type: ColorType, colorData?: ColorData): ColorData {
@@ -103,53 +72,38 @@ export function createTextureColor(colors: TextureImage, type: ColorType, colorD
     }
 }
 
-export interface InstanceColorProps {
-    colorFn: (instanceIdx: number) => Color
-    instanceCount: number
-}
-
 /** Creates color texture with color for each instance/unit */
-export function createInstanceColor(props: InstanceColorProps, colorData?: ColorData): ColorData {
-    const { colorFn, instanceCount} = props
+export function createInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+    const { instanceCount} = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= instanceCount * 3 ? colorData.tColor.ref.value : createTextureImage(instanceCount, 3)
-    for (let i = 0; i < instanceCount; i++) {
-        Color.toArray(colorFn(i), colors.array, i * 3)
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        Color.toArray(colorFn(v.location, v.isSecondary), colors.array, v.instanceIndex * 3)
+        locationIt.skipInstance()
     }
     return createTextureColor(colors, 'instance', colorData)
 }
 
-export interface ElementColorProps {
-    colorFn: (elementIdx: number) => Color
-    elementCount: number
-}
-
 /** Creates color texture with color for each element (i.e. shared across instances/units) */
-export function createElementColor(props: ElementColorProps, colorData?: ColorData): ColorData {
-    const { colorFn, elementCount } = props
+export function createElementColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+    const { elementCount } = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= elementCount * 3 ? colorData.tColor.ref.value : createTextureImage(elementCount, 3)
-    for (let i = 0, il = elementCount; i < il; ++i) {
-        Color.toArray(colorFn(i), colors.array, i * 3)
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        // console.log(v)
+        Color.toArray(colorFn(v.location, v.isSecondary), colors.array, v.elementIndex * 3)
     }
     return createTextureColor(colors, 'element', colorData)
 }
 
-export interface ElementInstanceColorProps {
-    colorFn: (instanceIdx: number, elementIdx: number) => Color
-    instanceCount: number,
-    elementCount: number
-}
-
 /** Creates color texture with color for each element instance (i.e. for each unit) */
-export function createElementInstanceColor(props: ElementInstanceColorProps, colorData?: ColorData): ColorData {
-    const { colorFn, instanceCount, elementCount } = props
+export function createElementInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+    const { elementCount, instanceCount } = locationIt
     const count = instanceCount * elementCount
     const colors = colorData && colorData.tColor.ref.value.array.length >= count * 3 ? colorData.tColor.ref.value : createTextureImage(count, 3)
-    let colorOffset = 0
-    for (let i = 0; i < instanceCount; i++) {
-        for (let j = 0, jl = elementCount; j < jl; ++j) {
-            Color.toArray(colorFn(i, j), colors.array, colorOffset)
-            colorOffset += 3
-        }
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        Color.toArray(colorFn(v.location, v.isSecondary), colors.array, v.index * 3)
     }
     return createTextureColor(colors, 'elementInstance', colorData)
 }

+ 1 - 1
src/mol-geo/util/marker-data.ts

@@ -37,7 +37,7 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
                 if (v % 2 !== 0) {
                     v -= 1
                     changed = true
-                } 
+                }
                 break
             case MarkerAction.Select:
                 v += 2

+ 82 - 27
src/mol-geo/util/size-data.ts

@@ -5,48 +5,103 @@
  */
 
 import { ValueCell } from 'mol-util';
-import VertexMap from '../shape/vertex-map';
+import { Vec2 } from 'mol-math/linear-algebra';
+import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
+import { LocationIterator } from '../representation/structure/visual/util/location-iterator';
+import { Location, NullLocation } from 'mol-model/location';
+
+export type SizeType = 'uniform' | 'instance' | 'element' | 'elementInstance'
 
 export type SizeData = {
     uSize: ValueCell<number>,
     aSize: ValueCell<Float32Array>,
+    tSize: ValueCell<TextureImage>,
+    uSizeTexSize: ValueCell<Vec2>,
     dSizeType: ValueCell<string>,
 }
 
-export interface UniformSizeProps {
-    value: number
-}
+export type LocationSize = (location: Location) => number
 
-/** Creates size uniform */
-export function createUniformSize(props: UniformSizeProps): SizeData {
+const emptySizeTexture = { array: new Uint8Array(1), width: 1, height: 1 }
+function createEmptySizeTexture() {
     return {
-        uSize: ValueCell.create(props.value),
-        aSize: ValueCell.create(new Float32Array(0)),
-        dSizeType: ValueCell.create('uniform'),
+        tSize: ValueCell.create(emptySizeTexture),
+        uSizeTexSize: ValueCell.create(Vec2.create(1, 1))
+    }
+}
+
+export function createValueSize(value: number, sizeData?: SizeData): SizeData {
+    if (sizeData) {
+        ValueCell.update(sizeData.uSize, value)
+        if (sizeData.dSizeType.ref.value !== 'uniform') {
+            ValueCell.update(sizeData.dSizeType, 'uniform')
+        }
+        return sizeData
+    } else {
+        return {
+            uSize: ValueCell.create(value),
+            aSize: ValueCell.create(new Float32Array(0)),
+            ...createEmptySizeTexture(),
+            dSizeType: ValueCell.create('uniform'),
+        }
     }
 }
 
-export interface AttributeSizeProps {
-    sizeFn: (elementIdx: number) => number
-    vertexMap: VertexMap
+/** Creates size uniform */
+export function createUniformSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+    return createValueSize(sizeFn(NullLocation), sizeData)
 }
 
-/** Creates size attribute with size for each element (i.e. shared across indtances/units) */
-export function createAttributeSize(props: AttributeSizeProps): SizeData {
-    const { sizeFn, vertexMap } = props
-    const { idCount, offsetCount, offsets } = vertexMap
-    const sizes = new Float32Array(idCount);
-    for (let i = 0, il = offsetCount - 1; i < il; ++i) {
-        const start = offsets[i]
-        const end = offsets[i + 1]
-        const size = sizeFn(i)
-        for (let i = start, il = end; i < il; ++i) {
-            sizes[i] = size
+export function createTextureSize(sizes: TextureImage, type: SizeType, sizeData?: SizeData): SizeData {
+    if (sizeData) {
+        ValueCell.update(sizeData.tSize, sizes)
+        ValueCell.update(sizeData.uSizeTexSize, Vec2.create(sizes.width, sizes.height))
+        if (sizeData.dSizeType.ref.value !== type) {
+            ValueCell.update(sizeData.dSizeType, type)
+        }
+        return sizeData
+    } else {
+        return {
+            uSize: ValueCell.create(0),
+            aSize: ValueCell.create(new Float32Array(0)),
+            tSize: ValueCell.create(sizes),
+            uSizeTexSize: ValueCell.create(Vec2.create(sizes.width, sizes.height)),
+            dSizeType: ValueCell.create(type),
         }
     }
-    return {
-        uSize: ValueCell.create(0),
-        aSize: ValueCell.create(sizes),
-        dSizeType: ValueCell.create('attribute'),
+}
+
+/** Creates size texture with size for each instance/unit */
+export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+    const { instanceCount} = locationIt
+    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= instanceCount ? sizeData.tSize.ref.value : createTextureImage(instanceCount, 1)
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        sizes.array[v.instanceIndex] = sizeFn(v.location)
+        locationIt.skipInstance()
+    }
+    return createTextureSize(sizes, 'instance', sizeData)
+}
+
+/** Creates size texture with size for each element (i.e. shared across instances/units) */
+export function createElementSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+    const { elementCount } = locationIt
+    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= elementCount ? sizeData.tSize.ref.value : createTextureImage(elementCount, 1)
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        sizes.array[v.elementIndex] = sizeFn(v.location)
+    }
+    return createTextureSize(sizes, 'element', sizeData)
+}
+
+/** Creates size texture with size for each element instance (i.e. for each unit) */
+export function createElementInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+    const { elementCount, instanceCount } = locationIt
+    const count = instanceCount * elementCount
+    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= count ? sizeData.tSize.ref.value : createTextureImage(count, 1)
+    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+        const v = locationIt.move()
+        sizes.array[v.index] = sizeFn(v.location)
     }
+    return createTextureSize(sizes, 'elementInstance', sizeData)
 }

+ 5 - 5
src/mol-gl/_spec/renderer.spec.ts

@@ -11,8 +11,8 @@ import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { ValueCell } from 'mol-util';
 
 import Renderer from '../renderer';
-import { createUniformColor } from 'mol-geo/util/color-data';
-import { createUniformSize } from 'mol-geo/util/size-data';
+import { createValueColor } from 'mol-geo/util/color-data';
+import { createValueSize } from 'mol-geo/util/size-data';
 import { createContext } from '../webgl/context';
 import { RenderableState } from '../renderable';
 import { createPointRenderObject } from '../render-object';
@@ -47,8 +47,8 @@ function createPoints() {
     const aPosition = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]))
     const aElementId = ValueCell.create(fillSerial(new Float32Array(3)))
     const aInstanceId = ValueCell.create(fillSerial(new Float32Array(1)))
-    const color = createUniformColor({ value: 0xFF0000 })
-    const size = createUniformSize({ value: 1 })
+    const color = createValueColor(0xFF0000)
+    const size = createValueSize(1)
     const marker = createEmptyMarkers()
 
     const aTransform = ValueCell.create(new Float32Array(16))
@@ -112,7 +112,7 @@ describe('renderer', () => {
 
         scene.add(points)
         expect(ctx.bufferCount).toBe(6);
-        expect(ctx.textureCount).toBe(2);
+        expect(ctx.textureCount).toBe(3);
         expect(ctx.vaoCount).toBe(4);
         expect(ctx.programCache.count).toBe(4);
         expect(ctx.shaderCache.count).toBe(8);

+ 3 - 1
src/mol-gl/renderable/point.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable'
 import { Context } from '../webgl/context';
 import { createRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, TextureSpec } from './schema';
 import { PointShaderCode } from '../shader-code';
 import { ValueCell } from 'mol-util';
 
@@ -15,6 +15,8 @@ export const PointSchema = {
     ...BaseSchema,
     aSize: AttributeSpec('float32', 1, 0),
     uSize: UniformSpec('f'),
+    uSizeTexSize: UniformSpec('v2'),
+    tSize: TextureSpec('alpha', 'ubyte'),
     dSizeType: DefineSpec('string', ['uniform', 'attribute']),
     dPointSizeAttenuation: DefineSpec('boolean'),
 }

+ 6 - 0
src/mol-gl/shader/point.vert

@@ -32,6 +32,12 @@ void main(){
         float size = uSize;
     #elif defined(dSizeType_attribute)
         float size = aSize;
+    #elif defined(dSizeType_instance)
+        float size = readFromTexture(tSize, aInstanceId, uSizeTexSize).r;
+    #elif defined(dSizeType_element)
+        float size = readFromTexture(tSize, aElementId, uSizeTexSize).r;
+    #elif defined(dSizeType_elementInstance)
+        float size = readFromTexture(tSize, aInstanceId * float(uElementCount) + aElementId, uSizeTexSize).r;
     #endif
 
     #ifdef dPointSizeAttenuation

+ 16 - 6
src/mol-math/linear-algebra/3d/mat4.ts

@@ -873,18 +873,28 @@ namespace Mat4 {
         return out;
     }
 
-    /** Rotation matrix for 90deg rotation around x-axis */
+    /** Rotation matrix for 90deg around x-axis */
     export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(1, 0, 0))
-    /** Rotation matrix for 90deg rotation around y-axis */
+    /** Rotation matrix for 180deg around x-axis */
+    export const rotX180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(1, 0, 0))
+    /** Rotation matrix for 90deg around y-axis */
     export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 1, 0))
-    /** Rotation matrix for 90deg rotation around z-axis */
+    /** Rotation matrix for 180deg around y-axis */
+    export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 1, 0))
+    /** Rotation matrix for 90deg around z-axis */
     export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 0, 1))
-    /** Rotation matrix for 90deg rotation around first x-axis and then y-axis */
+    /** Rotation matrix for 180deg around z-axis */
+    export const rotZ180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 0, 1))
+    /** Rotation matrix for 90deg around first x-axis and then y-axis */
     export const rotXY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotX90, rotY90)
-    /** Rotation matrix for 90deg rotation around first z-axis and then y-axis */
+    /** Rotation matrix for 90deg around first z-axis and then y-axis */
     export const rotZY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotY90)
-    /** Rotation matrix for 90deg rotation around first z-axis and then y-axis and then z-axis */
+    /** Rotation matrix for 90deg around first z-axis and then y-axis and then z-axis */
     export const rotZYZ90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZY90, rotZ90)
+    /** Rotation matrix for 90deg around first z-axis and then 180deg around x-axis */
+    export const rotZ90X180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotX180)
+    /** Rotation matrix for 90deg around first y-axis and then 180deg around z-axis */
+    export const rotY90Z180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotY90, rotZ180)
 }
 
 export default Mat4

+ 17 - 0
src/mol-model/location.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 { StructureElement } from './structure'
+import { Link } from './structure/structure/unit/links'
+
+/** A null value Location */
+export const NullLocation = { kind: 'null-location' as 'null-location' }
+export type NullLocation = typeof NullLocation
+export function isNullLocation(x: any): x is NullLocation {
+    return !!x && x.kind === 'null-location';
+}
+
+export type Location = StructureElement | Link.Location | NullLocation

+ 1 - 1
src/mol-model/loci.ts

@@ -14,7 +14,7 @@ export function isEveryLoci(x: any): x is EveryLoci {
     return !!x && x.kind === 'every-loci';
 }
 
-/** A Loci that that is empty */
+/** A Loci that is empty */
 export const EmptyLoci = { kind: 'empty-loci' as 'empty-loci' }
 export type EmptyLoci = typeof EmptyLoci
 export function isEmptyLoci(x: any): x is EmptyLoci {

+ 77 - 24
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -18,7 +18,7 @@ import StructureElement from '../element';
 import Structure from '../structure';
 import Unit from '../unit';
 import { SaccharideNameMap, UnknownSaccharideComponent } from './constants';
-import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink } from './data';
+import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink, PartialCarbohydrateElement } from './data';
 import { UnitRings, UnitRing } from '../unit/rings';
 import { ElementIndex } from '../../model/indexing';
 
@@ -49,7 +49,7 @@ function getAnomericCarbon(unit: Unit.Atomic, ringAtoms: ArrayLike<StructureElem
             // possibly an anomeric carbon if this is a mono-saccharide without a glycosidic bond
             indexHasOxygenAndCarbon = ei
         } else if (label_atom_id.value(ei).startsWith('C1')) {
-            // likely the anomeric carbon as it is name C1 by convention
+            // likely the anomeric carbon as it is named C1 by convention
             indexHasC1Name = ei
         } else {
             // use any carbon as a fallback
@@ -122,10 +122,11 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
     const links: CarbohydrateLink[] = []
     const terminalLinks: CarbohydrateTerminalLink[] = []
     const elements: CarbohydrateElement[] = []
+    const partialElements: PartialCarbohydrateElement[] = []
 
     const elementsWithRingMap = new Map<string, number>()
 
-    function elementKey(residueIndex: number, unitId: number, altId: string) {
+    function ringElementKey(residueIndex: number, unitId: number, altId: string) {
         return `${residueIndex}|${unitId}|${altId}`
     }
 
@@ -173,10 +174,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                 const sugarRings = filterFusedRings(unit.rings, sugarResidueMap.get(residueIndex));
 
                 if (!sugarRings || !sugarRings.length) {
-                    elements.push({
-                        hasRing: false,
-                        unit, residueIndex, component: saccharideComp
-                    })
+                    partialElements.push({ unit, residueIndex, component: saccharideComp })
                     continue;
                 }
 
@@ -196,11 +194,11 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                     const altId = getRingAltId(unit, ringAtoms)
                     const elementIndex = elements.length
                     ringElements.push(elementIndex)
-                    elementsWithRingMap.set(elementKey(residueIndex, unit.id, altId), elementIndex)
+                    elementsWithRingMap.set(ringElementKey(residueIndex, unit.id, altId), elementIndex)
                     elements.push({
                         geometry: { center, normal, direction },
-                        hasRing: true,
-                        unit, residueIndex, component: saccharideComp
+                        component: saccharideComp,
+                        unit, residueIndex, anomericCarbon
                     })
                 }
 
@@ -229,8 +227,8 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         }
     }
 
-    function getElementIndex(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
-        return elementsWithRingMap.get(elementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index)))
+    function getRingElementIndex(unit: Unit.Atomic, index: StructureElement.UnitIndex) {
+        return elementsWithRingMap.get(ringElementKey(unit.getResidueIndex(index), unit.id, getAltId(unit, index)))
     }
 
     // get carbohydrate links induced by inter-unit bonds
@@ -243,32 +241,32 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                 pairBonds.getBonds(indexA).forEach(bondInfo => {
                     const { unitA, unitB } = pairBonds
                     const indexB = bondInfo.indexB
-                    const elementIndexA = getElementIndex(unitA, indexA)
-                    const elementIndexB = getElementIndex(unitB, indexB)
+                    const ringElementIndexA = getRingElementIndex(unitA, indexA)
+                    const ringElementIndexB = getRingElementIndex(unitB, indexB)
 
-                    if (elementIndexA !== undefined && elementIndexB !== undefined) {
+                    if (ringElementIndexA !== undefined && ringElementIndexB !== undefined) {
                         const atomIdA = getAtomId(unitA, indexA)
                         if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
-                            fixLinkDirection(elementIndexA, elementIndexB)
+                            fixLinkDirection(ringElementIndexA, ringElementIndexB)
                         }
                         links.push({
-                            carbohydrateIndexA: elementIndexA,
-                            carbohydrateIndexB: elementIndexB
+                            carbohydrateIndexA: ringElementIndexA,
+                            carbohydrateIndexB: ringElementIndexB
                         })
-                    } else if (elementIndexA !== undefined) {
+                    } else if (ringElementIndexA !== undefined) {
                         const atomIdA = getAtomId(unitA, indexA)
                         if (atomIdA.startsWith('O1') || atomIdA.startsWith('C1')) {
-                            fixTerminalLinkDirection(elementIndexA, indexB, unitB)
+                            fixTerminalLinkDirection(ringElementIndexA, indexB, unitB)
                         }
                         terminalLinks.push({
-                            carbohydrateIndex: elementIndexA,
+                            carbohydrateIndex: ringElementIndexA,
                             elementIndex: indexB,
                             elementUnit: unitB,
                             fromCarbohydrate: true
                         })
-                    } else if (elementIndexB !== undefined) {
+                    } else if (ringElementIndexB !== undefined) {
                         terminalLinks.push({
-                            carbohydrateIndex: elementIndexB,
+                            carbohydrateIndex: ringElementIndexB,
                             elementIndex: indexA,
                             elementUnit: unitA,
                             fromCarbohydrate: false
@@ -279,5 +277,60 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         })
     }
 
-    return { links, terminalLinks, elements }
+    return { links, terminalLinks, elements, partialElements, ...buildLookups(elements, links) }
+}
+
+function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[]) {
+    // element lookup
+
+    function elementKey(unit: Unit, anomericCarbon: ElementIndex) {
+        return `${unit.id}|${anomericCarbon}`
+    }
+
+    const elementMap = new Map<string, number>()
+    for (let i = 0, il = elements.length; i < il; ++i) {
+        const { unit, anomericCarbon } = elements[i]
+        elementMap.set(elementKey(unit, anomericCarbon), i)
+    }
+
+    function getElementIndex(unit: Unit, anomericCarbon: ElementIndex) {
+        return elementMap.get(elementKey(unit, anomericCarbon))
+    }
+
+    // link lookup
+
+    function linkKey(unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) {
+        return `${unitA.id}|${anomericCarbonA}|${unitB.id}|${anomericCarbonB}`
+    }
+
+    const linkMap = new Map<string, number>()
+    for (let i = 0, il = links.length; i < il; ++i) {
+        const l = links[i]
+        const { unit: unitA, anomericCarbon: anomericCarbonA } = elements[l.carbohydrateIndexA]
+        const { unit: unitB, anomericCarbon: anomericCarbonB } = elements[l.carbohydrateIndexB]
+        linkMap.set(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB), i)
+    }
+
+    function getLinkIndex(unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) {
+        return linkMap.get(linkKey(unitA, anomericCarbonA, unitB, anomericCarbonB))
+    }
+
+    // anomeric carbon lookup
+
+    function anomericCarbonKey(unit: Unit, residueIndex: ResidueIndex) {
+        return `${unit.id}|${residueIndex}`
+    }
+
+    const anomericCarbonMap = new Map<string, ElementIndex>()
+    for (let i = 0, il = elements.length; i < il; ++i) {
+        const { unit, anomericCarbon } = elements[i]
+        const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[anomericCarbon]
+        anomericCarbonMap.set(anomericCarbonKey(unit, residueIndex), anomericCarbon)
+    }
+
+    function getAnomericCarbon(unit: Unit, residueIndex: ResidueIndex) {
+        return anomericCarbonMap.get(anomericCarbonKey(unit, residueIndex))
+    }
+
+    return { getElementIndex, getLinkIndex, getAnomericCarbon }
 }

+ 1 - 2
src/mol-model/structure/structure/carbohydrates/constants.ts

@@ -12,8 +12,7 @@ export const enum SaccharideShapes {
     FlatBox, FilledStar, FilledDiamond, FlatDiamond, FlatHexagon, Pentagon
 }
 
-// TODO move to theme
-const enum SaccharideColors {
+export const enum SaccharideColors {
     Blue = 0x0090bc,
     Green =	0x00a651,
     Yellow = 0xffd400,

+ 15 - 5
src/mol-model/structure/structure/carbohydrates/data.ts

@@ -6,7 +6,7 @@
 
 import Unit from '../unit';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { ResidueIndex } from '../../model';
+import { ResidueIndex, ElementIndex } from '../../model';
 import { SaccharideComponent } from './constants';
 
 export interface CarbohydrateLink {
@@ -23,16 +23,26 @@ export interface CarbohydrateTerminalLink {
 }
 
 export interface CarbohydrateElement {
-    // geometry is only defined if at least one ring is present.
-    readonly geometry?: { readonly center: Vec3, readonly normal: Vec3, readonly direction: Vec3 },
-    readonly hasRing: boolean,
+    readonly geometry: { readonly center: Vec3, readonly normal: Vec3, readonly direction: Vec3 },
+    readonly anomericCarbon: ElementIndex,
     readonly unit: Unit.Atomic,
     readonly residueIndex: ResidueIndex,
-    readonly component: SaccharideComponent
+    readonly component: SaccharideComponent,
+}
+
+// partial carbohydrate with no ring present
+export interface PartialCarbohydrateElement {
+    readonly unit: Unit.Atomic,
+    readonly residueIndex: ResidueIndex,
+    readonly component: SaccharideComponent,
 }
 
 export interface Carbohydrates {
     links: ReadonlyArray<CarbohydrateLink>
     terminalLinks: ReadonlyArray<CarbohydrateTerminalLink>
     elements: ReadonlyArray<CarbohydrateElement>
+    partialElements: ReadonlyArray<PartialCarbohydrateElement>
+    getElementIndex: (unit: Unit, anomericCarbon: ElementIndex) => number | undefined
+    getLinkIndex: (unitA: Unit, anomericCarbonA: ElementIndex, unitB: Unit, anomericCarbonB: ElementIndex) => number | undefined
+    getAnomericCarbon: (unit: Unit, residueIndex: ResidueIndex) => ElementIndex | undefined
 }

+ 8 - 1
src/mol-model/structure/structure/element.ts

@@ -10,13 +10,16 @@ import { ElementIndex } from '../model';
 import { ResidueIndex, ChainIndex } from '../model/indexing';
 
 interface StructureElement<U = Unit> {
+    readonly kind: 'element-location',
     unit: U,
     /** Index into element (atomic/coarse) properties of unit.model */
     element: ElementIndex
 }
 
 namespace StructureElement {
-    export function create(unit?: Unit, element?: ElementIndex): StructureElement { return { unit: unit as any, element: element || (0 as ElementIndex) }; }
+    export function create(unit?: Unit, element?: ElementIndex): StructureElement {
+        return { kind: 'element-location', unit: unit as any, element: element || (0 as ElementIndex) };
+    }
 
     // TODO: when nominal types are available, make this indexed by UnitIndex
     export type Set = SortedArray<ElementIndex>
@@ -60,6 +63,10 @@ namespace StructureElement {
         return !!x && x.kind === 'element-loci';
     }
 
+    export function isLocation(x: any): x is StructureElement {
+        return !!x && x.kind === 'element-location';
+    }
+
     export function residueIndex(e: StructureElement) {
         if (Unit.isAtomic(e.unit)) {
             return e.unit.residueIndex[e.element];

+ 16 - 6
src/mol-model/structure/structure/unit/links.ts

@@ -1,10 +1,11 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit } from '../../structure'
+import { Unit, StructureElement } from '../../structure'
 
 export * from './links/data'
 export * from './links/intra-compute'
@@ -12,12 +13,21 @@ export * from './links/inter-compute'
 
 namespace Link {
     export interface Location {
-        readonly aUnit: Unit,
+        readonly kind: 'link-location',
+        aUnit: Unit,
         /** Index into aUnit.elements */
-        readonly aIndex: number,
-        readonly bUnit: Unit,
+        aIndex: StructureElement.UnitIndex,
+        bUnit: Unit,
         /** Index into bUnit.elements */
-        readonly bIndex: number,
+        bIndex: StructureElement.UnitIndex,
+    }
+
+    export function Location(aUnit?: Unit, aIndex?: StructureElement.UnitIndex, bUnit?: Unit, bIndex?: StructureElement.UnitIndex): Location {
+        return { kind: 'link-location', aUnit: aUnit as any, aIndex: aIndex as any, bUnit: bUnit as any, bIndex: bIndex as any };
+    }
+
+    export function isLocation(x: any): x is Location {
+        return !!x && x.kind === 'link-location';
     }
 
     export interface Loci {

+ 6 - 5
src/mol-model/structure/structure/unit/pair-restraints/data.ts

@@ -5,6 +5,7 @@
  */
 
 import Unit from '../../unit';
+import { StructureElement } from '../../../structure';
 
 const emptyArray: number[] = []
 
@@ -13,13 +14,13 @@ class CrossLinkRestraints {
     private readonly pairKeyIndices: Map<string, number[]>
 
     /** Indices into this.pairs */
-    getPairIndices(indexA: number, unitA: Unit, indexB: number, unitB: Unit): ReadonlyArray<number> {
+    getPairIndices(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): ReadonlyArray<number> {
         const key = CrossLinkRestraints.getPairKey(indexA, unitA, indexB, unitB)
         const indices = this.pairKeyIndices.get(key)
         return indices !== undefined ? indices : emptyArray
     }
 
-    getPairs(indexA: number, unitA: Unit, indexB: number, unitB: Unit): CrossLinkRestraints.Pair[] | undefined {
+    getPairs(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): CrossLinkRestraints.Pair[] | undefined {
         const indices = this.getPairIndices(indexA, unitA, indexB, unitB)
         return indices.length ? indices.map(idx => this.pairs[idx]) : undefined
     }
@@ -42,8 +43,8 @@ namespace CrossLinkRestraints {
     export interface Pair {
         readonly unitA: Unit,
         readonly unitB: Unit,
-        readonly indexA: number,
-        readonly indexB: number,
+        readonly indexA: StructureElement.UnitIndex,
+        readonly indexB: StructureElement.UnitIndex,
 
         readonly restraintType: 'harmonic' | 'upper bound' | 'lower bound',
         readonly distanceThreshold: number,
@@ -52,7 +53,7 @@ namespace CrossLinkRestraints {
         readonly sigma2: number,
     }
 
-    export function getPairKey(indexA: number, unitA: Unit, indexB: number, unitB: Unit) {
+    export function getPairKey(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit) {
         return `${indexA}|${unitA.id}|${indexB}|${unitB.id}`
     }
 }

+ 7 - 6
src/mol-model/structure/structure/unit/pair-restraints/extract-cross-links.ts

@@ -8,6 +8,7 @@ import Unit from '../../unit';
 import Structure from '../../structure';
 import { IHMCrossLinkRestraint } from '../../../model/formats/mmcif/pair-restraint';
 import { CrossLinkRestraints } from './data';
+import { StructureElement } from '../../../structure';
 
 function _addRestraints(map: Map<number, number>, unit: Unit, restraints: IHMCrossLinkRestraint) {
     const { elements } = unit;
@@ -27,8 +28,8 @@ function extractInter(pairs: CrossLinkRestraints.Pair[], unitA: Unit, unitB: Uni
     const restraints = IHMCrossLinkRestraint.fromModel(unitA.model)
     if (!restraints) return
 
-    const rA = new Map<number, number>();
-    const rB = new Map<number, number>();
+    const rA = new Map<number, StructureElement.UnitIndex>();
+    const rB = new Map<number, StructureElement.UnitIndex>();
     _addRestraints(rA, unitA, restraints)
     _addRestraints(rB, unitB, restraints)
 
@@ -53,14 +54,14 @@ function extractIntra(pairs: CrossLinkRestraints.Pair[], unit: Unit) {
     const elementCount = elements.length;
     const kind = unit.kind
 
-    const r = new Map<number, number[]>();
+    const r = new Map<number, StructureElement.UnitIndex[]>();
 
     for (let i = 0; i < elementCount; i++) {
         const e = elements[i];
         restraints.getIndicesByElement(e, kind).forEach(ri => {
             const il = r.get(ri)
-            if (il) il.push(i)
-            else r.set(ri, [i])
+            if (il) il.push(i as StructureElement.UnitIndex)
+            else r.set(ri, [i as StructureElement.UnitIndex])
         })
     }
 
@@ -74,7 +75,7 @@ function extractIntra(pairs: CrossLinkRestraints.Pair[], unit: Unit) {
     })
 }
 
-function createCrossLinkRestraint(unitA: Unit, indexA: number, unitB: Unit, indexB: number, restraints: IHMCrossLinkRestraint, row: number): CrossLinkRestraints.Pair {
+function createCrossLinkRestraint(unitA: Unit, indexA: StructureElement.UnitIndex, unitB: Unit, indexB: StructureElement.UnitIndex, restraints: IHMCrossLinkRestraint, row: number): CrossLinkRestraints.Pair {
     return {
         unitA, indexA, unitB, indexB,
 

+ 30 - 18
src/mol-view/stage.ts

@@ -7,24 +7,25 @@
 import Viewer from './viewer'
 import { StateContext } from './state/context';
 import { Progress } from 'mol-task';
-import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick, StructureToDistanceRestraint, StructureToCartoon, StructureToBackbone, StructureCenter } from './state/transform';
+import { MmcifUrlToModel, ModelToStructure, StructureToSpacefill, StructureToBallAndStick, StructureToDistanceRestraint, StructureToCartoon, StructureToBackbone, StructureCenter, StructureToCarbohydrate } from './state/transform';
 import { UrlEntity } from './state/entity';
-import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
+import { SpacefillProps } from 'mol-geo/representation/structure/representation/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';
+import { BallAndStickProps } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { CartoonProps } from 'mol-geo/representation/structure/representation/cartoon';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/representation/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/representation/backbone';
+import { CarbohydrateProps } from 'mol-geo/representation/structure/representation/carbohydrate';
 // import { Queries as Q, StructureProperties as SP, Query, Selection } from 'mol-model/structure';
 
-const spacefillProps: SpacefillProps = {
+const spacefillProps: Partial<SpacefillProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     quality: 'auto',
     useFog: false
 }
 
-const ballAndStickProps: BallAndStickProps = {
+const ballAndStickProps: Partial<BallAndStickProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     sizeTheme: { name: 'uniform', value: 0.15 },
@@ -33,7 +34,7 @@ const ballAndStickProps: BallAndStickProps = {
     useFog: false
 }
 
-const distanceRestraintProps: DistanceRestraintProps = {
+const distanceRestraintProps: Partial<DistanceRestraintProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
     linkRadius: 0.5,
@@ -41,23 +42,32 @@ const distanceRestraintProps: DistanceRestraintProps = {
     useFog: false
 }
 
-const backboneProps: BackboneProps = {
+const backboneProps: Partial<BackboneProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
-    // colorTheme: { name: 'uniform', value: 0xFF0000 },
+    sizeTheme: { name: 'uniform', value: 0.3 },
     quality: 'auto',
     useFog: false,
     alpha: 0.5
 }
 
-const cartoonProps: CartoonProps = {
+const cartoonProps: Partial<CartoonProps> = {
     doubleSided: true,
     colorTheme: { name: 'chain-id' },
-    // colorTheme: { name: 'uniform', value: 0x2200CC },
+    sizeTheme: { name: 'uniform', value: 0.13, factor: 1 },
+    aspectRatio: 8,
     quality: 'auto',
     useFog: false
 }
 
+const carbohydrateProps: Partial<CarbohydrateProps> = {
+    doubleSided: true,
+    colorTheme: { name: 'carbohydrate-symbol' },
+    sizeTheme: { name: 'uniform', value: 1, factor: 1 },
+    quality: 'highest',
+    useFog: false
+}
+
 export class Stage {
     viewer: Viewer
     ctx = new StateContext(Progress.format)
@@ -78,7 +88,7 @@ export class Stage {
         // this.loadPdbid('1hrv') // viral assembly
         // this.loadPdbid('1rb8') // virus
         // this.loadPdbid('1blu') // metal coordination
-        // this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water, carbohydrates linked to protein
+        this.loadPdbid('3pqr') // inter unit bonds, two polymer chains, ligands, water, carbohydrates linked to protein
         // this.loadPdbid('4v5a') // ribosome
         // this.loadPdbid('3j3q') // ...
         // this.loadPdbid('2np2') // dna
@@ -105,12 +115,13 @@ export class Stage {
         // this.loadPdbid('2fnc') // contains maltotriose
         // this.loadPdbid('4zs9') // contains raffinose
         // this.loadPdbid('2yft') // contains kestose
-        this.loadPdbid('2b5t') // contains large carbohydrate polymer
+        // this.loadPdbid('2b5t') // contains large carbohydrate polymer
         // this.loadPdbid('1b5f') // contains carbohydrate with alternate locations
         // this.loadMmcifUrl(`../../examples/1cbs_full.bcif`)
         // this.loadMmcifUrl(`../../examples/1cbs_updated.cif`)
         // this.loadMmcifUrl(`../../examples/1crn.cif`)
-        // this.loadPdbid('1zag') // temp
+        // this.loadPdbid('5u0q') // mixed dna/rna in same polymer
+        // this.loadPdbid('5u0q') // temp
 
         // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok
         // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000002.cif`) // ok
@@ -138,6 +149,7 @@ export class Stage {
         StructureToDistanceRestraint.apply(this.ctx, structureEntity, { ...distanceRestraintProps, visible: false })
         StructureToBackbone.apply(this.ctx, structureEntity, { ...backboneProps, visible: false })
         StructureToCartoon.apply(this.ctx, structureEntity, { ...cartoonProps, visible: true })
+        StructureToCarbohydrate.apply(this.ctx, structureEntity, { ...carbohydrateProps, visible: true })
         StructureCenter.apply(this.ctx, structureEntity)
 
         this.globalContext.components.sequenceView.setState({ structure: structureEntity.value });
@@ -154,8 +166,8 @@ export class Stage {
     }
 
     loadPdbid (pdbid: string) {
-        return this.loadMmcifUrl(`http://www.ebi.ac.uk/pdbe/static/entry/${pdbid}_updated.cif`)
-        // return this.loadMmcifUrl(`https://files.rcsb.org/download/${pdbid}.cif`)
+        // return this.loadMmcifUrl(`http://www.ebi.ac.uk/pdbe/static/entry/${pdbid}_updated.cif`)
+        return this.loadMmcifUrl(`https://files.rcsb.org/download/${pdbid}.cif`)
     }
 
     dispose () {

+ 18 - 10
src/mol-view/state/entity.ts

@@ -12,11 +12,12 @@ import { CifFile, CifFrame } from 'mol-io/reader/cif';
 import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import { Model, Structure } from 'mol-model/structure';
 import { StructureRepresentation } from 'mol-geo/representation/structure';
-import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
-import { 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';
+import { SpacefillProps } from 'mol-geo/representation/structure/representation/spacefill';
+import { BallAndStickProps } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/representation/distance-restraint';
+import { CartoonProps } from 'mol-geo/representation/structure/representation/cartoon';
+import { BackboneProps } from 'mol-geo/representation/structure/representation/backbone';
+import { CarbohydrateProps } from 'mol-geo/representation/structure/representation/carbohydrate';
 
 const getNextId = idFactory(1)
 
@@ -121,34 +122,41 @@ export namespace StructureEntity {
 export type SpacefillEntity = StateEntity<StructureRepresentation<SpacefillProps>, 'spacefill'>
 export namespace SpacefillEntity {
     export function ofRepr(ctx: StateContext, repr: StructureRepresentation<SpacefillProps>): SpacefillEntity {
-        return StateEntity.create(ctx, 'spacefill', repr )
+        return StateEntity.create(ctx, 'spacefill', repr)
     }
 }
 
 export type BallAndStickEntity = StateEntity<StructureRepresentation<BallAndStickProps>, 'ballandstick'>
 export namespace BallAndStickEntity {
     export function ofRepr(ctx: StateContext, repr: StructureRepresentation<BallAndStickProps>): BallAndStickEntity {
-        return StateEntity.create(ctx, 'ballandstick', repr )
+        return StateEntity.create(ctx, 'ballandstick', repr)
     }
 }
 
 export type DistanceRestraintEntity = StateEntity<StructureRepresentation<DistanceRestraintProps>, 'distancerestraint'>
 export namespace DistanceRestraintEntity {
     export function ofRepr(ctx: StateContext, repr: StructureRepresentation<DistanceRestraintProps>): DistanceRestraintEntity {
-        return StateEntity.create(ctx, 'distancerestraint', repr )
+        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 )
+        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 )
+        return StateEntity.create(ctx, 'cartoon', repr)
+    }
+}
+
+export type CarbohydrateEntity = StateEntity<StructureRepresentation<CarbohydrateProps>, 'carbohydrate'>
+export namespace CarbohydrateEntity {
+    export function ofRepr(ctx: StateContext, repr: StructureRepresentation<CarbohydrateProps>): CarbohydrateEntity {
+        return StateEntity.create(ctx, 'carbohydrate', repr)
     }
 }

+ 87 - 63
src/mol-view/state/transform.ts

@@ -5,16 +5,17 @@
  */
 
 import CIF from 'mol-io/reader/cif'
-import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity, DistanceRestraintEntity, CartoonEntity, BackboneEntity } from './entity';
+import { FileEntity, DataEntity, UrlEntity, CifEntity, MmcifEntity, ModelEntity, StructureEntity, SpacefillEntity, AnyEntity, NullEntity, BallAndStickEntity, DistanceRestraintEntity, CartoonEntity, BackboneEntity, CarbohydrateEntity } from './entity';
 import { Model, Structure, Format } from 'mol-model/structure';
 
 import { StateContext } from './context';
 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';
+import { SpacefillProps, SpacefillRepresentation } from 'mol-geo/representation/structure/representation/spacefill';
+import { BallAndStickProps, BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { DistanceRestraintRepresentation, DistanceRestraintProps } from 'mol-geo/representation/structure/representation/distance-restraint';
+import { CartoonRepresentation, CartoonProps } from 'mol-geo/representation/structure/representation/cartoon';
+import { BackboneProps, BackboneRepresentation } from 'mol-geo/representation/structure/representation/backbone';
+import { CarbohydrateProps, CarbohydrateRepresentation } from 'mol-geo/representation/structure/representation/carbohydrate';
 
 type transformer<I extends AnyEntity, O extends AnyEntity, P extends {}> = (ctx: StateContext, inputEntity: I, props?: P) => Promise<O>
 
@@ -94,9 +95,9 @@ export const StructureCenter: StructureCenter = StateTransform.create('structure
         return NullEntity
     })
 
-export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEntity, SpacefillProps>
+export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEntity, Partial<SpacefillProps>>
 export const StructureToSpacefill: StructureToSpacefill = StateTransform.create('structure', 'spacefill', 'structure-to-spacefill',
-    async function (ctx: StateContext, structureEntity: StructureEntity, props: SpacefillProps = {}) {
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<SpacefillProps> = {}) {
         const spacefillRepr = SpacefillRepresentation()
         await spacefillRepr.create(structureEntity.value, props).run(ctx.log)
         ctx.viewer.add(spacefillRepr)
@@ -105,9 +106,9 @@ export const StructureToSpacefill: StructureToSpacefill = StateTransform.create(
         return SpacefillEntity.ofRepr(ctx, spacefillRepr)
     })
 
-export type StructureToBallAndStick = StateTransform<StructureEntity, BallAndStickEntity, BallAndStickProps>
+export type StructureToBallAndStick = StateTransform<StructureEntity, BallAndStickEntity, Partial<BallAndStickProps>>
 export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.create('structure', 'ballandstick', 'structure-to-ballandstick',
-    async function (ctx: StateContext, structureEntity: StructureEntity, props: BallAndStickProps = {}) {
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<BallAndStickProps> = {}) {
         const ballAndStickRepr = BallAndStickRepresentation()
         await ballAndStickRepr.create(structureEntity.value, props).run(ctx.log)
         ctx.viewer.add(ballAndStickRepr)
@@ -116,9 +117,9 @@ export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.c
         return BallAndStickEntity.ofRepr(ctx, ballAndStickRepr)
     })
 
-export type StructureToDistanceRestraint = StateTransform<StructureEntity, DistanceRestraintEntity, DistanceRestraintProps>
+export type StructureToDistanceRestraint = StateTransform<StructureEntity, DistanceRestraintEntity, Partial<DistanceRestraintProps>>
 export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateTransform.create('structure', 'distancerestraint', 'structure-to-distancerestraint',
-    async function (ctx: StateContext, structureEntity: StructureEntity, props: DistanceRestraintProps = {}) {
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<DistanceRestraintProps> = {}) {
         const distanceRestraintRepr = DistanceRestraintRepresentation()
         await distanceRestraintRepr.create(structureEntity.value, props).run(ctx.log)
         ctx.viewer.add(distanceRestraintRepr)
@@ -126,31 +127,43 @@ export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateT
         console.log('stats', ctx.viewer.stats)
         return DistanceRestraintEntity.ofRepr(ctx, distanceRestraintRepr)
     })
-export type StructureToBackbone = StateTransform<StructureEntity, BackboneEntity, BackboneProps>
+
+export type StructureToBackbone = StateTransform<StructureEntity, BackboneEntity, Partial<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>
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<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, Partial<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>
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<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 StructureToCarbohydrate = StateTransform<StructureEntity, CarbohydrateEntity, Partial<CarbohydrateProps>>
+export const StructureToCarbohydrate: StructureToCarbohydrate = StateTransform.create('structure', 'carbohydrate', 'structure-to-cartoon',
+    async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<CarbohydrateProps> = {}) {
+        const carbohydrateRepr = CarbohydrateRepresentation()
+        await carbohydrateRepr.create(structureEntity.value, props).run(ctx.log)
+        ctx.viewer.add(carbohydrateRepr)
+        ctx.viewer.requestDraw()
+        console.log('stats', ctx.viewer.stats)
+        return CarbohydrateEntity.ofRepr(ctx, carbohydrateRepr)
+    })
+
+export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, Partial<SpacefillProps>>
 export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update',
-    async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: SpacefillProps = {}) {
+    async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: Partial<SpacefillProps> = {}) {
         const spacefillRepr = spacefillEntity.value
         await spacefillRepr.update(props).run(ctx.log)
         ctx.viewer.add(spacefillRepr)
@@ -159,9 +172,9 @@ export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill
         return NullEntity
     })
 
-export type BallAndStickUpdate = StateTransform<BallAndStickEntity, NullEntity, BallAndStickProps>
+export type BallAndStickUpdate = StateTransform<BallAndStickEntity, NullEntity, Partial<BallAndStickProps>>
 export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('ballandstick', 'null', 'ballandstick-update',
-    async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: BallAndStickProps = {}) {
+    async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: Partial<BallAndStickProps> = {}) {
         const ballAndStickRepr = ballAndStickEntity.value
         await ballAndStickRepr.update(props).run(ctx.log)
         ctx.viewer.add(ballAndStickRepr)
@@ -170,9 +183,9 @@ export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('bal
         return NullEntity
     })
 
-export type DistanceRestraintUpdate = StateTransform<DistanceRestraintEntity, NullEntity, DistanceRestraintProps>
+export type DistanceRestraintUpdate = StateTransform<DistanceRestraintEntity, NullEntity, Partial<DistanceRestraintProps>>
 export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.create('distancerestraint', 'null', 'distancerestraint-update',
-    async function (ctx: StateContext, distanceRestraintEntity: DistanceRestraintEntity, props: DistanceRestraintProps = {}) {
+    async function (ctx: StateContext, distanceRestraintEntity: DistanceRestraintEntity, props: Partial<DistanceRestraintProps> = {}) {
         const distanceRestraintRepr = distanceRestraintEntity.value
         await distanceRestraintRepr.update(props).run(ctx.log)
         ctx.viewer.add(distanceRestraintRepr)
@@ -181,27 +194,38 @@ export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.c
         return NullEntity
     })
 
-export type BackboneUpdate = StateTransform<BackboneEntity, NullEntity, BackboneProps>
+export type BackboneUpdate = StateTransform<BackboneEntity, NullEntity, Partial<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>
+    async function (ctx: StateContext, backboneEntity: BackboneEntity, props: Partial<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, Partial<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
-        })
+    async function (ctx: StateContext, cartoonEntity: CartoonEntity, props: Partial<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
+    })
+
+export type CarbohydrateUpdate = StateTransform<CarbohydrateEntity, NullEntity, Partial<CarbohydrateProps>>
+export const CarbohydrateUpdate: CarbohydrateUpdate = StateTransform.create('carbohydrate', 'null', 'carbohydrate-update',
+    async function (ctx: StateContext, carbohydrateEntity: CarbohydrateEntity, props: Partial<CarbohydrateProps> = {}) {
+        const carbohydrateRepr = carbohydrateEntity.value
+        await carbohydrateRepr.update(props).run(ctx.log)
+        ctx.viewer.add(carbohydrateRepr)
+        ctx.viewer.requestDraw()
+        console.log('stats', ctx.viewer.stats)
+        return NullEntity
+    })
 
 // composed
 
@@ -227,24 +251,24 @@ export const DataToModel: DataToModel = StateTransform.create('data', 'model', '
         return MmcifToModel.apply(ctx, mmcifEntity)
     })
 
-export type ModelToSpacefill = StateTransform<ModelEntity, SpacefillEntity, SpacefillProps>
+export type ModelToSpacefill = StateTransform<ModelEntity, SpacefillEntity, Partial<SpacefillProps>>
 export const ModelToSpacefill: ModelToSpacefill = StateTransform.create('model', 'spacefill', 'model-to-spacefill',
-    async function (ctx: StateContext, modelEntity: ModelEntity, props: SpacefillProps = {}) {
+    async function (ctx: StateContext, modelEntity: ModelEntity, props: Partial<SpacefillProps> = {}) {
         const structureEntity = await ModelToStructure.apply(ctx, modelEntity)
         // StructureToBond.apply(ctx, structureEntity, props)
         return StructureToSpacefill.apply(ctx, structureEntity, props)
     })
 
-export type MmcifUrlToSpacefill = StateTransform<UrlEntity, SpacefillEntity, SpacefillProps>
+export type MmcifUrlToSpacefill = StateTransform<UrlEntity, SpacefillEntity, Partial<SpacefillProps>>
 export const MmcifUrlToSpacefill: MmcifUrlToSpacefill = StateTransform.create('url', 'spacefill', 'url-to-spacefill',
-    async function (ctx: StateContext, urlEntity: UrlEntity, props: SpacefillProps = {}) {
+    async function (ctx: StateContext, urlEntity: UrlEntity, props: Partial<SpacefillProps> = {}) {
         const modelEntity = await MmcifUrlToModel.apply(ctx, urlEntity)
         return ModelToSpacefill.apply(ctx, modelEntity, props)
     })
 
-export type MmcifFileToSpacefill = StateTransform<FileEntity, SpacefillEntity, SpacefillProps>
+export type MmcifFileToSpacefill = StateTransform<FileEntity, SpacefillEntity, Partial<SpacefillProps>>
 export const MmcifFileToSpacefill: MmcifFileToSpacefill = StateTransform.create('file', 'spacefill', 'file-to-spacefill',
-    async function (ctx: StateContext, fileEntity: FileEntity, props: SpacefillProps = {}) {
+    async function (ctx: StateContext, fileEntity: FileEntity, props: Partial<SpacefillProps> = {}) {
         const modelEntity = await MmcifFileToModel.apply(ctx, fileEntity)
         return ModelToSpacefill.apply(ctx, modelEntity, props)
     })

+ 50 - 0
src/mol-view/theme/color.ts

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color } from 'mol-util/color';
+import { Structure } from 'mol-model/structure';
+import { ColorType, LocationColor } from 'mol-geo/util/color-data';
+
+import { ElementIndexColorTheme } from './color/element-index';
+import { CarbohydrateSymbolColorTheme } from './color/carbohydrate-symbol';
+import { ChainIdColorTheme } from './color/chain-id';
+import { ElementSymbolColorTheme } from './color/element-symbol';
+import { UnitIndexColorTheme } from './color/unit-index';
+import { UniformColorTheme } from './color/uniform';
+
+export interface ColorTheme {
+    kind: ColorType
+    color: LocationColor
+}
+
+export function ColorTheme(props: ColorThemeProps): ColorTheme {
+    switch (props.name) {
+        case 'element-index': return ElementIndexColorTheme(props)
+        case 'carbohydrate-symbol': return CarbohydrateSymbolColorTheme(props)
+        case 'chain-id': return ChainIdColorTheme(props)
+        case 'element-symbol': return ElementSymbolColorTheme(props)
+        case 'unit-index': return UnitIndexColorTheme(props)
+        case 'uniform': return UniformColorTheme(props)
+    }
+}
+
+export interface ColorThemeProps {
+    name: 'element-index' | 'chain-id'| 'unit-index' | 'uniform' | 'carbohydrate-symbol' | 'element-symbol'
+    domain?: [number, number]
+    value?: Color
+    structure?: Structure
+}
+
+export const ColorThemeInfo = {
+    'element-index': {},
+    'carbohydrate-symbol': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'unit-index': {},
+    'uniform': {}
+}
+export type ColorThemeName = keyof typeof ColorThemeInfo
+export const ColorThemeNames = Object.keys(ColorThemeInfo)

+ 52 - 0
src/mol-view/theme/color/carbohydrate-symbol.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 { StructureElement, Link, ElementIndex, Unit } from 'mol-model/structure';
+
+import { SaccharideColors } from 'mol-model/structure/structure/carbohydrates/constants';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme } from '../color';
+import { LocationColor } from 'mol-geo/util/color-data';
+
+const DefaultColor = 0xCCCCCC;
+
+export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme {
+    let colorFn: LocationColor
+
+    if (props.structure) {
+        const { elements, getElementIndex, getAnomericCarbon } = props.structure.carbohydrates
+
+        const getColor = (unit: Unit, index: ElementIndex) => {
+            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index[index]
+            const anomericCarbon = getAnomericCarbon(unit, residueIndex)
+            if (anomericCarbon !== undefined) {
+                const idx = getElementIndex(unit, anomericCarbon)
+                if (idx !== undefined) return elements[idx].component.color
+            }
+            return DefaultColor
+        }
+
+        colorFn = (location: Location, isSecondary: boolean) => {
+            if (isSecondary) {
+                return SaccharideColors.Secondary
+            } else {
+                if (StructureElement.isLocation(location)) {
+                    return getColor(location.unit, location.element)
+                } else if (Link.isLocation(location)) {
+                    return getColor(location.aUnit, location.aUnit.elements[location.aIndex])
+                }
+            }
+            return DefaultColor
+        }
+    } else {
+        colorFn = () => DefaultColor
+    }
+
+    return {
+        kind: 'element',
+        color: colorFn
+    }
+}

+ 47 - 0
src/mol-view/theme/color/chain-id.ts

@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, StructureProperties, StructureElement, Link } from 'mol-model/structure';
+
+import { ColorScale, Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme } from '../color';
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.label_asym_id
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}
+
+export function ChainIdColorTheme(props: ColorThemeProps): ColorTheme {
+    const l = StructureElement.create()
+
+    function colorFn(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            const map = location.unit.model.properties.asymIdSerialMap
+            const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] })
+            const asym_id = getAsymId(location.unit)
+            return scale.color(map.get(asym_id(location)) || 0)
+        } else if (Link.isLocation(location)) {
+            const map = location.aUnit.model.properties.asymIdSerialMap
+            const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] })
+            const asym_id = getAsymId(location.aUnit)
+            l.unit = location.aUnit
+            l.element = location.aUnit.elements[location.aIndex]
+            return scale.color(map.get(asym_id(l)) || 0)
+        }
+        return 0
+    }
+
+    return {
+        kind: 'element',
+        color: colorFn
+    }
+}

+ 50 - 0
src/mol-view/theme/color/element-index.ts

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ColorScale, Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { StructureElement, Link, Unit } from 'mol-model/structure';
+import { OrderedSet } from 'mol-data/int';
+import { LocationColor } from 'mol-geo/util/color-data';
+import { ColorThemeProps, ColorTheme } from '../color';
+
+const DefaultColor = 0xCCCCCC;
+
+export function ElementIndexColorTheme(props: ColorThemeProps): ColorTheme {
+    let colorFn: LocationColor
+
+    if (props.structure) {
+        const { units } = props.structure
+        const unitCount = units.length
+        const cummulativeElementCount = new Map<number, number>()
+
+        let elementCount = 0
+        for (let i = 0; i < unitCount; ++i) {
+            cummulativeElementCount.set(i, elementCount)
+            elementCount += units[i].elements.length
+        }
+        const scale = ColorScale.create({ domain: [ 0, elementCount ] })
+
+        colorFn = (location: Location): Color => {
+            if (StructureElement.isLocation(location)) {
+                const unitIndex = Unit.findUnitById(location.unit.id, units)
+                const unitElementIndex = OrderedSet.findPredecessorIndex(location.unit.elements, location.element)
+                return scale.color(cummulativeElementCount.get(unitIndex) || 0 + unitElementIndex)
+            } else if (Link.isLocation(location)) {
+                const unitId = Unit.findUnitById(location.aUnit.id, units)
+                return scale.color(cummulativeElementCount.get(unitId) || 0 + location.aIndex)
+            }
+            return 0
+        }
+    } else {
+        colorFn = () => DefaultColor
+    }
+
+    return {
+        kind: 'elementInstance',
+        color: colorFn
+    }
+}

+ 23 - 12
src/mol-geo/theme/structure/color/element-symbol.ts → src/mol-view/theme/color/element-symbol.ts

@@ -6,8 +6,9 @@
 
 import { ElementSymbol } from 'mol-model/structure/model/types';
 import { Color } from 'mol-util/color';
-import { StructureColorDataProps } from '.';
-import { createElementColor, ColorData } from '../../../util/color-data';
+import { StructureElement, Unit, Link } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorThemeProps, ColorTheme } from '../color';
 
 // from Jmol http://jmol.sourceforge.net/jscolors/ (or 0xFFFFFF)
 export const ElementSymbolColors: { [k: string]: Color } = {
@@ -21,14 +22,24 @@ export function elementSymbolColor(element: ElementSymbol): Color {
     return c === void 0 ? DefaultElementSymbolColor : c
 }
 
-export function elementSymbolColorData(props: StructureColorDataProps, colorData?: ColorData) {
-    const { group: { units, elements }, elementCount } = props
-    const { type_symbol } = units[0].model.atomicHierarchy.atoms
-    return createElementColor({
-        colorFn: (elementIdx: number) => {
-            const e = elements[elementIdx]
-            return elementSymbolColor(type_symbol.value(e))
-        },
-        elementCount
-    }, colorData)
+export function ElementSymbolColorTheme(props: ColorThemeProps): ColorTheme {
+    function colorFn(location: Location): Color {
+        if (StructureElement.isLocation(location)) {
+            if (Unit.isAtomic(location.unit)) {
+                const { type_symbol } = location.unit.model.atomicHierarchy.atoms
+                return elementSymbolColor(type_symbol.value(location.element))
+            }
+        } else if (Link.isLocation(location)) {
+            if (Unit.isAtomic(location.aUnit)) {
+                const { type_symbol } = location.aUnit.model.atomicHierarchy.atoms
+                return elementSymbolColor(type_symbol.value(location.aUnit.elements[location.aIndex]))
+            }
+        }
+        return DefaultElementSymbolColor
+    }
+
+    return {
+        kind: 'element',
+        color: colorFn
+    }
 }

+ 18 - 0
src/mol-view/theme/color/uniform.ts

@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ColorTheme, ColorThemeProps } from '../color';
+
+const DefaultColor = 0xCCCCCC;
+
+export function UniformColorTheme(props: ColorThemeProps): ColorTheme {
+    const color = props.value || DefaultColor
+
+    return {
+        kind: 'uniform',
+        color: () => color
+    }
+}

+ 40 - 0
src/mol-view/theme/color/unit-index.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ColorScale, Color } from 'mol-util/color';
+import { Location } from 'mol-model/location';
+import { Unit, StructureElement, Link } from 'mol-model/structure';
+import { LocationColor } from 'mol-geo/util/color-data';
+import { ColorTheme, ColorThemeProps } from '../color';
+
+const DefaultColor = 0xCCCCCC;
+
+export function UnitIndexColorTheme(props: ColorThemeProps): ColorTheme {
+    let colorFn: LocationColor
+
+    if (props.structure) {
+        const { units } = props.structure
+        const unitCount = units.length
+
+        const scale = ColorScale.create({ domain: [ 0, unitCount ] })
+
+        colorFn = (location: Location): Color => {
+            if (StructureElement.isLocation(location)) {
+                return scale.color(Unit.findUnitById(location.unit.id, units))
+            } else if (Link.isLocation(location)) {
+                return scale.color(Unit.findUnitById(location.aUnit.id, units))
+            }
+            return 0
+        }
+    } else {
+        colorFn = () => DefaultColor
+    }
+
+    return {
+        kind: 'instance',
+        color: colorFn
+    }
+}

+ 37 - 0
src/mol-view/theme/size.ts

@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Structure } from 'mol-model/structure';
+import { SizeType, LocationSize } from 'mol-geo/util/size-data';
+
+import { PhysicalSizeTheme } from './size/physical';
+import { UniformSizeTheme } from './size/uniform';
+
+export interface SizeTheme {
+    kind: SizeType
+    size: LocationSize
+}
+
+export function SizeTheme(props: SizeThemeProps): SizeTheme {
+    switch (props.name) {
+        case 'physical': return PhysicalSizeTheme(props)
+        case 'uniform': return UniformSizeTheme(props)
+    }
+}
+
+export interface SizeThemeProps {
+    name: 'physical' | 'uniform'
+    value?: number
+    factor?: number
+    structure?: Structure
+}
+
+export const SizeThemeInfo = {
+    'physical': {},
+    'uniform': {}
+}
+export type SizeThemeName = keyof typeof SizeThemeInfo
+export const SizeThemeNames = Object.keys(SizeThemeInfo)

+ 53 - 0
src/mol-view/theme/size/physical.ts

@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureElement, Unit, StructureProperties, Link } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { SizeThemeProps, SizeTheme } from '../size';
+
+const DefaultSize = 1
+const DefaultFactor = 1
+
+export function getPhysicalRadius(unit: Unit): StructureElement.Property<number> {
+    if (Unit.isAtomic(unit)) {
+        return StructureProperties.atom.vdw_radius
+    } else if (Unit.isSpheres(unit)) {
+        return StructureProperties.coarse.sphere_radius
+    } else {
+        return () => 0
+    }
+}
+
+/**
+ * Create attribute data with the physical size of an element,
+ * i.e. vdw for atoms and radius for coarse spheres
+ */
+export function PhysicalSizeTheme(props: SizeThemeProps): SizeTheme {
+    const factor = props.factor || DefaultFactor
+    const l = StructureElement.create()
+
+    function sizeFn(location: Location): number {
+        if (StructureElement.isLocation(location)) {
+            if (Unit.isAtomic(location.unit)) {
+                const radius = getPhysicalRadius(location.unit)
+                return factor * radius(location)
+            }
+        } else if (Link.isLocation(location)) {
+            if (Unit.isAtomic(location.aUnit)) {
+                const radius = getPhysicalRadius(location.aUnit)
+                l.unit = location.aUnit
+                l.element = location.aUnit.elements[location.aIndex]
+                return factor * radius(l)
+            }
+        }
+        return DefaultSize
+    }
+
+    return {
+        kind: 'element',
+        size: sizeFn
+    }
+}

+ 21 - 0
src/mol-view/theme/size/uniform.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { SizeTheme, SizeThemeProps } from '../size';
+
+const DefaultSize = 1
+const DefaultFactor = 1
+
+export function UniformSizeTheme(props: SizeThemeProps): SizeTheme {
+    const value = props.value || DefaultSize
+    const factor = props.factor || DefaultFactor
+    const size = value * factor
+
+    return {
+        kind: 'uniform',
+        size: () => size
+    }
+}

Some files were not shown because too many files changed in this diff