Browse Source

Merge branch 'custom-repr' of https://github.com/molstar/molstar-proto

David Sehnal 6 years ago
parent
commit
dddf6dde8e
100 changed files with 2969 additions and 1024 deletions
  1. 25 19
      package-lock.json
  2. 6 6
      package.json
  3. 52 0
      src/apps/canvas/app.ts
  4. 99 0
      src/apps/canvas/assembly-symmetry.ts
  5. 85 0
      src/apps/canvas/component/app.tsx
  6. 128 0
      src/apps/canvas/component/structure-representation.tsx
  7. 198 0
      src/apps/canvas/component/structure-view.tsx
  8. 101 0
      src/apps/canvas/component/viewport.tsx
  9. 22 4
      src/apps/canvas/index.html
  10. 13 143
      src/apps/canvas/index.ts
  11. 371 0
      src/apps/canvas/structure-view.ts
  12. 46 0
      src/apps/canvas/util.ts
  13. 0 6
      src/apps/viewer/index.tsx
  14. 14 9
      src/mol-app/ui/visualization/viewport.tsx
  15. 15 0
      src/mol-data/db/table.ts
  16. 1 0
      src/mol-data/int/impl/interval.ts
  17. 1 0
      src/mol-data/int/impl/ordered-set.ts
  18. 1 0
      src/mol-data/int/impl/sorted-array.ts
  19. 1 0
      src/mol-data/int/interval.ts
  20. 1 0
      src/mol-data/int/ordered-set.ts
  21. 1 0
      src/mol-data/int/sorted-array.ts
  22. 1 1
      src/mol-data/int/sorted-ranges.ts
  23. 14 1
      src/mol-data/util/hash-functions.ts
  24. 65 0
      src/mol-geo/mesh/builder/bounding-box.ts
  25. 3 3
      src/mol-geo/mesh/builder/tube.ts
  26. 6 7
      src/mol-geo/representation/index.ts
  27. 23 19
      src/mol-geo/representation/shape/index.ts
  28. 16 43
      src/mol-geo/representation/structure/complex-representation.ts
  29. 63 39
      src/mol-geo/representation/structure/complex-visual.ts
  30. 3 0
      src/mol-geo/representation/structure/index.ts
  31. 11 12
      src/mol-geo/representation/structure/representation/backbone.ts
  32. 20 22
      src/mol-geo/representation/structure/representation/ball-and-stick.ts
  33. 21 19
      src/mol-geo/representation/structure/representation/carbohydrate.ts
  34. 31 30
      src/mol-geo/representation/structure/representation/cartoon.ts
  35. 11 12
      src/mol-geo/representation/structure/representation/distance-restraint.ts
  36. 47 0
      src/mol-geo/representation/structure/representation/point.ts
  37. 34 2
      src/mol-geo/representation/structure/representation/spacefill.ts
  38. 78 60
      src/mol-geo/representation/structure/units-representation.ts
  39. 96 47
      src/mol-geo/representation/structure/units-visual.ts
  40. 13 11
      src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts
  41. 10 6
      src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
  42. 1 1
      src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
  43. 127 94
      src/mol-geo/representation/structure/visual/element-point.ts
  44. 1 0
      src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
  45. 9 5
      src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
  46. 23 20
      src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
  47. 9 6
      src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
  48. 6 8
      src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
  49. 19 24
      src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
  50. 6 8
      src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
  51. 74 40
      src/mol-geo/representation/structure/visual/util/common.ts
  52. 6 5
      src/mol-geo/representation/structure/visual/util/element.ts
  53. 5 3
      src/mol-geo/representation/structure/visual/util/link.ts
  54. 72 0
      src/mol-geo/representation/structure/visual/util/nucleotide.ts
  55. 101 33
      src/mol-geo/representation/structure/visual/util/polymer.ts
  56. 10 3
      src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts
  57. 2 8
      src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts
  58. 16 15
      src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts
  59. 43 24
      src/mol-geo/representation/util.ts
  60. 12 11
      src/mol-geo/representation/volume/index.ts
  61. 4 5
      src/mol-geo/representation/volume/surface.ts
  62. 28 13
      src/mol-geo/util/color-data.ts
  63. 4 1
      src/mol-geo/util/location-iterator.ts
  64. 1 18
      src/mol-geo/util/marker-data.ts
  65. 20 4
      src/mol-geo/util/size-data.ts
  66. 52 0
      src/mol-geo/util/transform-data.ts
  67. 3 3
      src/mol-gl/renderable/util.ts
  68. 18 6
      src/mol-gl/renderer.ts
  69. 2 2
      src/mol-gl/scene.ts
  70. 17 8
      src/mol-gl/shader-code.ts
  71. 1 1
      src/mol-gl/shader/chunks/assign-color-varying.glsl
  72. 1 1
      src/mol-gl/shader/chunks/assign-material-color.glsl
  73. 5 1
      src/mol-gl/shader/chunks/common-frag-params.glsl
  74. 3 0
      src/mol-gl/shader/chunks/common-vert-params.glsl
  75. 1 3
      src/mol-gl/shader/mesh.frag
  76. 0 2
      src/mol-gl/shader/mesh.vert
  77. 2 4
      src/mol-gl/shader/point.frag
  78. 4 0
      src/mol-gl/shader/point.vert
  79. 13 3
      src/mol-gl/webgl/program.ts
  80. 6 4
      src/mol-gl/webgl/render-item.ts
  81. 1 1
      src/mol-math/geometry/lookup3d/grid.ts
  82. 44 11
      src/mol-math/geometry/primitives/box3d.ts
  83. 11 4
      src/mol-math/geometry/primitives/sphere3d.ts
  84. 3 3
      src/mol-math/geometry/symmetry-operator.ts
  85. 7 0
      src/mol-math/linear-algebra/3d/mat4.ts
  86. 26 6
      src/mol-model-props/rcsb/symmetry.ts
  87. 15 0
      src/mol-model/loci.ts
  88. 11 0
      src/mol-model/shape/shape.ts
  89. 6 1
      src/mol-model/structure/model/formats/mmcif.ts
  90. 3 3
      src/mol-model/structure/model/formats/mmcif/assembly.ts
  91. 13 0
      src/mol-model/structure/structure/carbohydrates/constants.ts
  92. 11 0
      src/mol-model/structure/structure/element.ts
  93. 11 2
      src/mol-model/structure/structure/structure.ts
  94. 5 13
      src/mol-model/structure/structure/symmetry.ts
  95. 112 25
      src/mol-model/structure/structure/unit.ts
  96. 15 0
      src/mol-model/structure/structure/unit/links.ts
  97. 5 0
      src/mol-model/structure/structure/unit/links/data.ts
  98. 86 47
      src/mol-model/structure/structure/util/boundary.ts
  99. 35 0
      src/mol-model/structure/structure/util/nucleotide.ts
  100. 75 0
      src/mol-model/structure/structure/util/polymer.ts

+ 25 - 19
package-lock.json

@@ -138,9 +138,9 @@
       "dev": true
     },
     "@types/node": {
-      "version": "10.7.1",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.7.1.tgz",
-      "integrity": "sha512-EGoI4ylB/lPOaqXqtzAyL8HcgOuCtH2hkEaLmkueOYufsTFWBn4VCvlCDC2HW8Q+9iF+QVC3sxjDKQYjHQeZ9w==",
+      "version": "10.9.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz",
+      "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==",
       "dev": true
     },
     "@types/node-fetch": {
@@ -174,9 +174,9 @@
       "dev": true
     },
     "@types/react": {
-      "version": "16.4.11",
-      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.11.tgz",
-      "integrity": "sha512-1DQnmwO8u8N3ucvRX2ZLDEjQ2VctkAvL/rpbm2ev4uaZA0z4ysU+I0tk+K8ZLblC6p7MCgFyF+cQlSNIPUHzeQ==",
+      "version": "16.4.13",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-16.4.13.tgz",
+      "integrity": "sha512-a2Z7UmwnAzZ23bTHV6on141S8vvSC7MEJGG85R5/VG80ybzkt5QJqNzlaJ0Y6OX1dncrXFW8B0vWPIx7QuOUqA==",
       "dev": true,
       "requires": {
         "@types/prop-types": "*",
@@ -8175,9 +8175,9 @@
       }
     },
     "rxjs": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.2.2.tgz",
-      "integrity": "sha512-0MI8+mkKAXZUF9vMrEoPnaoHkfzBPP4IGwUYRJhIRJF6/w3uByO1e91bEHn8zd43RdkTMKiooYKmwz7RH6zfOQ==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.1.tgz",
+      "integrity": "sha512-hRVfb1Mcf8rLXq1AZEjYpzBnQbO7Duveu1APXkWRTvqzhmkoQ40Pl2F9Btacx+gJCOqsMiugCGG4I2HPQgJRtA==",
       "requires": {
         "tslib": "^1.9.0"
       }
@@ -9460,9 +9460,9 @@
       }
     },
     "style-loader": {
-      "version": "0.22.1",
-      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.22.1.tgz",
-      "integrity": "sha512-WXUrLeinPIR1Oat3PfCDro7qTniwNTJqGqv1KcQiL3JR5PzrVLTyNsd9wTsPXG/qNCJ7lzR2NY/QDjFsP7nuSQ==",
+      "version": "0.23.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.0.tgz",
+      "integrity": "sha512-uCcN7XWHkqwGVt7skpInW6IGO1tG6ReyFQ1Cseh0VcN6VdcFQi62aG/2F3Y9ueA8x4IVlfaSUxpmQXQD9QrEuQ==",
       "dev": true,
       "requires": {
         "loader-utils": "^1.1.0",
@@ -10105,9 +10105,9 @@
       "dev": true
     },
     "typescript": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz",
-      "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz",
+      "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==",
       "dev": true
     },
     "ua-parser-js": {
@@ -10116,15 +10116,21 @@
       "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA=="
     },
     "uglify-js": {
-      "version": "3.4.7",
-      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.7.tgz",
-      "integrity": "sha512-J0M2i1mQA+ze3EdN9SBi751DNdAXmeFLfJrd/MDIkRc3G3Gbb9OPVSx7GIQvVwfWxQARcYV2DTxIkMyDAk3o9Q==",
+      "version": "3.4.9",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+      "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
       "dev": true,
       "requires": {
-        "commander": "~2.16.0",
+        "commander": "~2.17.1",
         "source-map": "~0.6.1"
       },
       "dependencies": {
+        "commander": {
+          "version": "2.17.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+          "dev": true
+        },
         "source-map": {
           "version": "0.6.1",
           "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",

+ 6 - 6
package.json

@@ -76,9 +76,9 @@
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.0",
     "@types/jest": "^23.3.1",
-    "@types/node": "^10.7.1",
+    "@types/node": "^10.9.4",
     "@types/node-fetch": "^2.1.2",
-    "@types/react": "^16.4.11",
+    "@types/react": "^16.4.13",
     "@types/react-dom": "^16.0.7",
     "benchmark": "^2.1.4",
     "cpx": "^1.5.0",
@@ -97,11 +97,11 @@
     "raw-loader": "^0.5.1",
     "resolve-url-loader": "^2.3.0",
     "sass-loader": "^7.1.0",
-    "style-loader": "^0.22.1",
+    "style-loader": "^0.23.0",
     "ts-jest": "^23.1.4",
     "tslint": "^5.11.0",
-    "typescript": "^3.0.1",
-    "uglify-js": "^3.4.7",
+    "typescript": "^3.0.3",
+    "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
     "webpack": "^4.17.1",
     "webpack-cli": "^3.1.0"
@@ -116,6 +116,6 @@
     "node-fetch": "^2.2.0",
     "react": "^16.4.2",
     "react-dom": "^16.4.2",
-    "rxjs": "^6.2.2"
+    "rxjs": "^6.3.1"
   }
 }

+ 52 - 0
src/apps/canvas/app.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 Viewer from 'mol-view/viewer';
+import { getCifFromUrl, getModelsFromMmcif, getCifFromFile } from './util';
+import { StructureView } from './structure-view';
+import { BehaviorSubject } from 'rxjs';
+import { CifBlock } from 'mol-io/reader/cif';
+
+export class App {
+    viewer: Viewer
+    container: HTMLDivElement | null = null;
+    canvas: HTMLCanvasElement | null = null;
+    structureView: StructureView | null = null;
+
+    pdbIdLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null)
+
+    initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) {
+        this.canvas = _canvas
+        this.container = _container
+
+        try {
+            this.viewer = Viewer.create(this.canvas, this.container)
+            this.viewer.animate()
+            return true
+        } catch (e) {
+            console.error(e)
+            return false
+        }
+    }
+
+    async loadCif(cif: CifBlock, assemblyId?: string) {
+        const models = await getModelsFromMmcif(cif)
+        this.structureView = await StructureView(this.viewer, models, { assemblyId })
+        this.pdbIdLoaded.next(this.structureView)
+    }
+
+    async loadPdbId(id: string, assemblyId?: string) {
+        if (this.structureView) this.structureView.destroy()
+        const cif = await getCifFromUrl(`https://files.rcsb.org/download/${id}.cif`)
+        this.loadCif(cif, assemblyId)
+    }
+
+    async loadCifFile(file: File) {
+        if (this.structureView) this.structureView.destroy()
+        const cif = await getCifFromFile(file)
+        this.loadCif(cif)
+    }
+}

+ 99 - 0
src/apps/canvas/assembly-symmetry.ts

@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { Table } from 'mol-data/db';
+import { Color, ColorScale } from 'mol-util/color';
+import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
+import { Tensor } from 'mol-math/linear-algebra';
+import { addSphere } from 'mol-geo/mesh/builder/sphere';
+import { addCylinder } from 'mol-geo/mesh/builder/cylinder';
+import { Shape } from 'mol-model/shape';
+import { ColorTheme } from 'mol-view/theme/color';
+import { Location } from 'mol-model/location';
+import { StructureElement, Unit, StructureProperties } from 'mol-model/structure';
+
+export function getAxesShape(featureId: number, assemblySymmetry: AssemblySymmetry) {
+    const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature
+    const feature = Table.pickRow(f, i => f.id.value(i) === featureId)
+    if (!feature) return
+
+    const axes = assemblySymmetry.getAxes(featureId)
+    if (!axes._rowCount) return
+
+    const vectorSpace = AssemblySymmetry.Schema.rcsb_assembly_symmetry_axis.start.space;
+
+    const colors: Color[] = []
+    const labels: string[] = []
+
+    const radius = 0.4
+    const cylinderProps = { radiusTop: radius, radiusBottom: radius }
+    const meshBuilder = MeshBuilder.create(256, 128)
+
+    for (let i = 0, il = axes._rowCount; i < il; ++i) {
+        const start = Tensor.toVec3(vectorSpace, axes.start.value(i))
+        const end = Tensor.toVec3(vectorSpace, axes.end.value(i))
+        meshBuilder.setGroup(i)
+        addSphere(meshBuilder, start, radius, 2)
+        addSphere(meshBuilder, end, radius, 2)
+        addCylinder(meshBuilder, start, end, 1, cylinderProps)
+        colors.push(Color(0xCCEE11))
+        labels.push(`Axis ${i + 1} for ${feature.symmetry_value} ${feature.type.toLowerCase()} symmetry`)
+    }
+    const mesh = meshBuilder.getMesh()
+    const shape = Shape.create('Axes', mesh, colors, labels)
+    return shape
+}
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.auth_asym_id // TODO
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}
+
+function memberKey (asym_id: string, oper_list_id?: number) {
+    return `${asym_id}|${oper_list_id}`
+}
+
+export function getClusterColorTheme(featureId: number, assemblySymmetry: AssemblySymmetry): ColorTheme {
+    const DefaultColor = Color(0xCCCCCC)
+    const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature
+    const feature = Table.pickRow(f, i => f.id.value(i) === featureId)
+    if (!feature) return { granularity: 'uniform', color: () => DefaultColor }
+
+    const clusters = assemblySymmetry.getClusters(featureId)
+    if (!clusters._rowCount) return { granularity: 'uniform', color: () => DefaultColor }
+
+    const clusterByMember = new Map<string, number>()
+    for (let i = 0, il = clusters._rowCount; i < il; ++i) {
+        clusters.members.value(i).forEach(m => {
+            const ms = m.split('_')
+            const asym_id = ms[0]
+            const oper_list_id = ms.length === 2 ? parseInt(ms[1]) : undefined
+            clusterByMember.set(memberKey(asym_id, oper_list_id), i)
+        })
+    }
+
+    const scale = ColorScale.create({ domain: [ 0, clusters._rowCount - 1 ] })
+
+    return {
+        granularity: 'instance',
+        color: (location: Location): Color => {
+            if (StructureElement.isLocation(location)) {
+                const ns = location.unit.conformation.operator.name.split('-')
+                const asym_id = getAsymId(location.unit)
+                const oper_list_id = ns.length === 2 ? parseInt(ns[1]) : undefined
+                const cluster = clusterByMember.get(memberKey(asym_id(location), oper_list_id))
+                return cluster !== undefined ? scale.color(cluster) : DefaultColor
+            }
+            return DefaultColor
+        }
+    }
+}

+ 85 - 0
src/apps/canvas/component/app.tsx

@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../structure-view';
+import { App } from '../app';
+import { Viewport } from './viewport';
+import { StructureViewComponent } from './structure-view';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface AppProps {
+    app: App
+}
+
+export interface AppState {
+    structureView: StructureView | null
+}
+
+export class AppComponent extends React.Component<AppProps, AppState> {
+    state = {
+        structureView: this.props.app.structureView,
+    }
+
+    componentDidMount() {
+        this.props.app.pdbIdLoaded.subscribe((structureView) => {
+            this.setState({
+                structureView: this.props.app.structureView
+            })
+        })
+    }
+
+    render() {
+        const { structureView } = this.state
+
+        return <div style={{width: '100%', height: '100%'}}>
+            <div style={{left: '0px', right: '350px', height: '100%', position: 'absolute'}}>
+                <Viewport app={this.props.app} />
+            </div>
+
+            <div style={{width: '330px', paddingLeft: '10px', paddingRight: '10px', right: '0px', height: '100%', position: 'absolute', overflow: 'auto'}}>
+                <div style={{marginTop: '10px'}}>
+                    <span>Load PDB ID </span>
+                    <input
+                        type='text'
+                        onKeyDown={e => {
+                            if (e.keyCode === 13) {
+                                const value = e.currentTarget.value.trim()
+                                if (value) {
+                                    this.props.app.loadPdbId(value)
+                                }
+                            }
+                        }}
+                    />
+                </div>
+                <div>
+                    <span>Load CIF file </span>
+                    <input
+                        accept='*.cif'
+                        type='file'
+                        onChange={e => {
+                            if (e.target.files) this.props.app.loadCifFile(e.target.files[0])
+                        }}
+                    />
+                </div>
+                <hr/>
+                <div style={{marginBottom: '10px'}}>
+                    {structureView ? <StructureViewComponent structureView={structureView} /> : ''}
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 128 - 0
src/apps/canvas/component/structure-representation.tsx

@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureRepresentation, StructureProps } from 'mol-geo/representation/structure';
+import Viewer from 'mol-view/viewer';
+import { VisualQuality, VisualQualityNames } from 'mol-geo/representation/util';
+import { ColorThemeProps, ColorThemeName, ColorThemeNames, ColorTheme } from 'mol-view/theme/color';
+import { Color } from 'mol-util/color';
+
+export interface StructureRepresentationComponentProps {
+    viewer: Viewer
+    representation: StructureRepresentation<StructureProps>
+}
+
+export interface StructureRepresentationComponentState {
+    label: string
+    visible: boolean
+    quality: VisualQuality
+    colorTheme: ColorThemeProps
+}
+
+export class StructureRepresentationComponent extends React.Component<StructureRepresentationComponentProps, StructureRepresentationComponentState> {
+    state = {
+        label: this.props.representation.label,
+        visible: this.props.representation.props.visible,
+        quality: this.props.representation.props.quality,
+        colorTheme: this.props.representation.props.colorTheme,
+    }
+
+    componentWillMount() {
+        const repr = this.props.representation
+
+        this.setState({
+            ...this.state,
+            label: repr.label,
+            visible: repr.props.visible,
+            quality: repr.props.quality,
+            colorTheme: repr.props.colorTheme,
+        })
+    }
+
+    async update(state: Partial<StructureRepresentationComponentState>) {
+        const repr = this.props.representation
+        const props: Partial<StructureProps> = {}
+
+        if (state.visible !== undefined) props.visible = state.visible
+        if (state.quality !== undefined) props.quality = state.quality
+        if (state.colorTheme !== undefined) props.colorTheme = state.colorTheme
+
+        await repr.createOrUpdate(props).run()
+        this.props.viewer.add(repr)
+        this.props.viewer.requestDraw(true)
+        console.log(this.props.viewer.stats)
+
+        const newState = {
+            ...this.state,
+            visible: repr.props.visible,
+            quality: repr.props.quality,
+            colorTheme: repr.props.colorTheme,
+        }
+        this.setState(newState)
+    }
+
+    render() {
+        const { label, visible, quality, colorTheme } = this.state
+
+        const ct = ColorTheme(colorTheme)
+
+        if (ct.legend && ct.legend.kind === 'scale-legend') {
+            // console.log(`linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`)
+        }
+
+        return <div>
+            <div>
+                <h4>{label}</h4>
+            </div>
+            <div>
+                <div>
+                    <span>Visible </span>
+                    <button onClick={(e) => this.update({ visible: !visible }) }>
+                        {visible ? 'Hide' : 'Show'}
+                    </button>
+                </div>
+                <div>
+                    <span>Quality </span>
+                    <select value={quality} onChange={(e) => this.update({ quality: e.target.value as VisualQuality }) }>
+                        {VisualQualityNames.map(name => <option key={name} value={name}>{name}</option>)}
+                    </select>
+                </div>
+                <div>
+                    <span>Color Theme </span>
+                    <select value={colorTheme.name} onChange={(e) => this.update({ colorTheme: { name: e.target.value as ColorThemeName } }) }>
+                        {ColorThemeNames.map(name => <option key={name} value={name}>{name}</option>)}
+                    </select>
+                    {ct.description ? <div><i>{ct.description}</i></div> : ''}
+                    {
+                        ct.legend && ct.legend.kind === 'scale-legend'
+                            ? <div
+                                style={{
+                                    width: '100%',
+                                    height: '30px',
+                                    background: `linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`
+                                }}
+                            >
+                                <span style={{float: 'left', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.min}</span>
+                                <span style={{float: 'right', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.max}</span>
+                            </div>
+                        : ct.legend && ct.legend.kind === 'table-legend'
+                            ? <div>
+                                {ct.legend.table.map((value, i) => {
+                                    const [name, color] = value
+                                    return <div key={i} style={{minWidth: '60px', marginRight: '5px', display: 'inline-block'}}>
+                                        <div style={{width: '30px', height: '20px', backgroundColor: Color.toStyle(color), display: 'inline-block'}}></div>
+                                        {name}
+                                    </div>
+                                })}
+                            </div>
+                        : ''
+                    }
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 198 - 0
src/apps/canvas/component/structure-view.tsx

@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../structure-view';
+import { StructureRepresentation } from 'mol-geo/representation/structure';
+import { StructureRepresentationComponent } from './structure-representation';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface StructureViewComponentProps {
+    structureView: StructureView
+}
+
+export interface StructureViewComponentState {
+    structureView: StructureView
+
+    label: string
+    modelId: number
+    modelIds: { id: number, label: string }[]
+    assemblyId: string
+    assemblyIds: { id: string, label: string }[]
+    symmetryFeatureId: number
+    symmetryFeatureIds: { id: number, label: string }[]
+
+    active: { [k: string]: boolean }
+    structureRepresentations: { [k: string]: StructureRepresentation<any> }
+}
+
+export class StructureViewComponent extends React.Component<StructureViewComponentProps, StructureViewComponentState> {
+    state = this.stateFromStructureView(this.props.structureView)
+
+    private stateFromStructureView(sv: StructureView) {
+        return {
+            structureView: sv,
+
+            label: sv.label,
+            modelId: sv.modelId,
+            modelIds: sv.getModelIds(),
+            assemblyId: sv.assemblyId,
+            assemblyIds: sv.getAssemblyIds(),
+            symmetryFeatureId: sv.symmetryFeatureId,
+            symmetryFeatureIds: sv.getSymmetryFeatureIds(),
+
+            active: sv.active,
+            structureRepresentations: sv.structureRepresentations
+        }
+    }
+
+    componentWillMount() {
+        this.setState(this.stateFromStructureView(this.props.structureView))
+    }
+
+    componentDidMount() {
+        const sv = this.props.structureView
+
+        this.props.structureView.updated.subscribe(() => this.setState({
+            symmetryFeatureIds: sv.getSymmetryFeatureIds(),
+            structureRepresentations: sv.structureRepresentations
+        }))
+    }
+
+    componentWillReceiveProps(nextProps: StructureViewComponentProps) {
+        if (nextProps.structureView !== this.props.structureView) {
+            this.setState(this.stateFromStructureView(nextProps.structureView))
+
+            nextProps.structureView.updated.subscribe(() => this.setState({
+                symmetryFeatureIds: nextProps.structureView.getSymmetryFeatureIds(),
+                structureRepresentations: nextProps.structureView.structureRepresentations
+            }))
+        }
+    }
+
+    async update(state: Partial<StructureViewComponentState>) {
+        const sv = this.state.structureView
+
+        if (state.modelId !== undefined) await sv.setModel(state.modelId)
+        if (state.assemblyId !== undefined) await sv.setAssembly(state.assemblyId)
+        if (state.symmetryFeatureId !== undefined) await sv.setSymmetryFeature(state.symmetryFeatureId)
+
+        this.setState(this.stateFromStructureView(sv))
+    }
+
+    render() {
+        const { structureView, label, modelIds, assemblyIds, symmetryFeatureIds, active, structureRepresentations } = this.state
+
+        const modelIdOptions = modelIds.map(m => {
+            return <option key={m.id} value={m.id}>{m.label}</option>
+        })
+        const assemblyIdOptions = assemblyIds.map(a => {
+            return <option key={a.id} value={a.id}>{a.label}</option>
+        })
+        const symmetryFeatureIdOptions = symmetryFeatureIds.map(f => {
+            return <option key={f.id} value={f.id}>{f.label}</option>
+        })
+
+        return <div>
+            <div>
+                <h2>{label}</h2>
+            </div>
+            <div>
+                <div>
+                    <span>Model </span>
+                    <select
+                        style={{width: '100px'}}
+                        value={this.state.modelId}
+                        onChange={(e) => {
+                            this.update({ modelId: parseInt(e.target.value) })
+                        }}
+                    >
+                        {modelIdOptions}
+                    </select>
+                    <span> </span>
+                    <input type='range'
+                        value={this.state.modelId}
+                        min={Math.min(...modelIds.map(m => m.id))}
+                        max={Math.max(...modelIds.map(m => m.id))}
+                        step='1'
+                        onInput={(e) => {
+                            this.update({ modelId: parseInt(e.currentTarget.value) })
+                        }}
+                    >
+                    </input>
+                </div>
+                <div>
+                    <span>Assembly </span>
+                    <select
+                        style={{width: '150px'}}
+                        value={this.state.assemblyId}
+                        onChange={(e) => {
+                            this.update({ assemblyId: e.target.value })
+                        }}
+                    >
+                        {assemblyIdOptions}
+                    </select>
+                </div>
+                <div>
+                    <span>Symmetry Feature </span>
+                    <select
+                        style={{width: '150px'}}
+                        value={this.state.symmetryFeatureId}
+                        onChange={(e) => {
+                            this.update({ symmetryFeatureId: parseInt(e.target.value) })
+                        }}
+                    >
+                        {symmetryFeatureIdOptions}
+                    </select>
+                </div>
+                <div>
+                    <h4>Active</h4>
+                    { Object.keys(active).map((k, i) => {
+                        return <div key={i}>
+                            <input
+                                type='checkbox'
+                                checked={active[k]}
+                                onChange={(e) => {
+                                    const sv = structureView
+                                    if (k === 'symmetryAxes') {
+                                        sv.setSymmetryAxes(e.target.checked)
+                                    } else if (Object.keys(sv.structureRepresentations).includes(k)) {
+                                        sv.setStructureRepresentation(k, e.target.checked)
+                                    }
+                                }}
+                            /> {k}
+                        </div>
+                    } ) }
+                </div>
+                <div>
+                    <h3>Structure Representations</h3>
+                    { Object.keys(structureRepresentations).map((k, i) => {
+                        if (active[k]) {
+                            return <div key={i}>
+                                <StructureRepresentationComponent
+                                    representation={structureRepresentations[k]}
+                                    viewer={structureView.viewer}
+                                />
+                            </div>
+                        } else {
+                            return ''
+                        }
+                    } ) }
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 101 - 0
src/apps/canvas/component/viewport.tsx

@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { App } from '../app';
+import { MarkerAction } from 'mol-geo/util/marker-data';
+import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci';
+import { labelFirst } from 'mol-view/label';
+
+interface ViewportProps {
+    app: App
+}
+
+interface ViewportState {
+    noWebGl: boolean,
+    info: string
+}
+
+export class Viewport extends React.Component<ViewportProps, ViewportState> {
+    private container: HTMLDivElement | null = null;
+    private canvas: HTMLCanvasElement | null = null;
+
+    state: ViewportState = {
+        noWebGl: false,
+        info: ''
+    };
+
+    handleResize() {
+        this.props.app.viewer.handleResize()
+    }
+
+    componentDidMount() {
+        if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) {
+            this.setState({ noWebGl: true });
+        }
+        this.handleResize()
+
+        const viewer = this.props.app.viewer
+
+        viewer.input.resize.subscribe(() => this.handleResize())
+
+        let prevLoci: Loci = EmptyLoci
+        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) return
+            const p = viewer.identify(x, y)
+            if (p) {
+                const loci = viewer.getLoci(p)
+
+                if (!areLociEqual(loci, prevLoci)) {
+                    viewer.mark(prevLoci, MarkerAction.RemoveHighlight)
+                    viewer.mark(loci, MarkerAction.Highlight)
+                    prevLoci = loci
+
+                    const label = labelFirst(loci)
+                    const info = `${label}`
+                    this.setState({ info })
+                }
+            }
+        })
+    }
+
+    componentWillUnmount() {
+        if (super.componentWillUnmount) super.componentWillUnmount();
+        // TODO viewer cleanup
+    }
+
+    renderMissing() {
+        return <div>
+            <div>
+                <p><b>WebGL does not seem to be available.</b></p>
+                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p>
+                <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
+            </div>
+        </div>
+    }
+
+    render() {
+        if (this.state.noWebGl) return this.renderMissing();
+
+        return <div style={{ backgroundColor: 'rgb(0, 0, 0)', width: '100%', height: '100%'}}>
+            <div ref={elm => this.container = elm} style={{width: '100%', height: '100%'}}>
+                <canvas ref={elm => this.canvas = elm}></canvas>
+            </div>
+            <div
+                style={{
+                    position: 'absolute',
+                    top: 10,
+                    left: 10,
+                    padding: 10,
+                    color: 'lightgrey',
+                    background: 'rgba(0, 0, 0, 0.2)'
+                }}
+            >
+                {this.state.info}
+            </div>
+        </div>;
+    }
+}

+ 22 - 4
src/apps/canvas/index.html

@@ -4,12 +4,30 @@
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
         <title>Mol* Canvas</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+            }
+            html, body {
+                width: 100%;
+                height: 100%;
+                overflow: hidden;
+            }
+            hr {
+                margin: 10px;
+            }
+            h1, h2, h3, h4, h5 {
+                margin-top: 5px;
+                margin-bottom: 3px;
+            }
+            button {
+                padding: 2px;
+            }
+        </style>
     </head>
     <body>
-        <div id="container" style="width:800px; height: 600px;">
-            <canvas id="canvas"></canvas>
-        </div>
-        <span id="info"></span>
+        <div id="app" style="width: 100%; height: 100%"></div>
         <script type="text/javascript" src="./index.js"></script>
     </body>
 </html>

+ 13 - 143
src/apps/canvas/index.ts

@@ -4,151 +4,21 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import './index.html'
-
-import Viewer from 'mol-view/viewer';
-import CIF, { CifBlock } from 'mol-io/reader/cif'
-// import { parse as parseObj } from 'mol-io/reader/obj/parser'
-import { readUrlAs } from 'mol-util/read'
-import { Model, Format, Structure, StructureSymmetry } from 'mol-model/structure';
-import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon';
-import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
-import { EveryLoci } from 'mol-model/loci';
-import { MarkerAction } from 'mol-geo/util/marker-data';
-import { labelFirst } from 'mol-view/label';
-import { Queries as Q, StructureProperties as SP, StructureSelection, StructureQuery } from 'mol-model/structure';
-import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
-import { ShapeRepresentation } from 'mol-geo/representation/shape';
-import { Vec3, Mat4 } from 'mol-math/linear-algebra';
-import { Shape } from 'mol-model/shape';
-import { Color } from 'mol-util/color';
-import { addSphere } from 'mol-geo/mesh/builder/sphere';
-import { Box } from 'mol-geo/primitive/box';
-
-const container = document.getElementById('container')
-if (!container) throw new Error('Can not find element with id "container".')
-
-const canvas = document.getElementById('canvas') as HTMLCanvasElement
-if (!canvas) throw new Error('Can not find element with id "canvas".')
-
-const info = document.getElementById('info') as HTMLCanvasElement
-if (!info) throw new Error('Can not find element with id "info".')
-
-const viewer = Viewer.create(canvas, container)
-viewer.animate()
-
-viewer.input.resize.subscribe(() => {
-    // do whatever appropriate
-})
-
-viewer.input.move.subscribe(({x, y, inside, buttons}) => {
-    if (!inside || buttons) return
-    const p = viewer.identify(x, y)
-    const loci = viewer.getLoci(p)
-
-    viewer.mark(EveryLoci, MarkerAction.RemoveHighlight)
-    viewer.mark(loci, MarkerAction.Highlight)
-
-    const label = labelFirst(loci)
-    info.innerText = `${label}`
-})
-
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
 
-// async function getObjFromUrl(url: string) {
-//     const data = await readUrlAs(url, false) as string
-//     const comp = parseObj(data)
-//     const parsed = await comp.run()
-//     if (parsed.isError) throw parsed
-//     return parsed.result
-// }
-
-async function getCifFromUrl(url: string) {
-    const data = await readUrlAs(url, false)
-    const comp = CIF.parse(data)
-    const parsed = await comp.run()
-    if (parsed.isError) throw parsed
-    return parsed.result.blocks[0]
-}
-
-async function getModelFromMmcif(cif: CifBlock) {
-    const models = await Model.create(Format.mmCIF(cif)).run()
-    return models[0]
-}
-
-async function getStructureFromModel(model: Model, assembly = '1') {
-    const assemblies = model.symmetry.assemblies
-    if (assemblies.length) {
-        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
-    } else {
-        return Structure.ofModel(model)
-    }
-}
-
-async function init() {
-    const cif = await getCifFromUrl('https://files.rcsb.org/download/1crn.cif')
-    const model = await getModelFromMmcif(cif)
-    const structure = await getStructureFromModel(model)
-
-    viewer.center(structure.boundary.sphere.center)
-
-    // cartoon for whole structure
-    const cartoonRepr = CartoonRepresentation()
-    await cartoonRepr.create(structure, {
-        colorTheme: { name: 'chain-id' },
-        sizeTheme: { name: 'uniform', value: 0.2 },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(cartoonRepr)
-
-    // create new structure via query
-    const q1 = Q.generators.atoms({
-        residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7
-    });
-    const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure));
-
-    // ball+stick for new structure
-    const ballStickRepr = BallAndStickRepresentation()
-    await ballStickRepr.create(newStructure, {
-        colorTheme: { name: 'element-symbol' },
-        sizeTheme: { name: 'uniform', value: 0.1 },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(ballStickRepr)
-
-    // create a mesh
-    const meshBuilder = MeshBuilder.create(256, 128)
-    const colors: Color[] = []
-    const labels: string[] = []
-    // red sphere
-    meshBuilder.setGroup(0)
-    colors[0] = Color(0xFF2233)
-    labels[0] = 'red sphere'
-    addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2)
-    // green cube
-    meshBuilder.setGroup(1)
-    colors[1] = Color(0x2233FF)
-    labels[1] = 'blue cube'
-    const t = Mat4.identity()
-    Mat4.fromTranslation(t, Vec3.create(10, 0, 0))
-    Mat4.scale(t, t, Vec3.create(3, 3, 3))
-    meshBuilder.add(t, Box())
-    const mesh = meshBuilder.getMesh()
-    // const mesh = getObjFromUrl('mesh.obj')
+import './index.html'
 
-    // create shape from mesh
-    const shape = Shape.create('myShape', mesh, colors, labels)
+import { App } from './app';
+import { AppComponent } from './component/app';
+import { urlQueryParameter } from 'mol-util/url-query';
 
-    // add representation from shape
-    const customRepr = ShapeRepresentation()
-    await customRepr.create(shape, {
-        colorTheme: { name: 'shape-group' },
-        // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(customRepr)
+const elm = document.getElementById('app') as HTMLElement
+if (!elm) throw new Error('Can not find element with id "app".')
 
-    // ensure the added representations get rendered, i.e. without mouse input
-    viewer.requestDraw()
-}
+const app = new App()
+ReactDOM.render(React.createElement(AppComponent, { app }), elm);
 
-init()
+const assemblyId = urlQueryParameter('assembly')
+const pdbId = urlQueryParameter('pdb')
+if (pdbId) app.loadPdbId(pdbId, assemblyId)

+ 371 - 0
src/apps/canvas/structure-view.ts

@@ -0,0 +1,371 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Model, Structure } from 'mol-model/structure';
+import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon';
+import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { getStructureFromModel } from './util';
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { ShapeRepresentation, ShapeProps } from 'mol-geo/representation/shape';
+import { getAxesShape } from './assembly-symmetry';
+import Viewer from 'mol-view/viewer';
+import { CarbohydrateRepresentation } from 'mol-geo/representation/structure/representation/carbohydrate';
+// import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
+// import { addSphere } from 'mol-geo/mesh/builder/sphere';
+// import { Shape } from 'mol-model/shape';
+// import { Color } from 'mol-util/color';
+// import { computeUnitBoundary } from 'mol-model/structure/structure/util/boundary';
+// import { addBoundingBox } from 'mol-geo/mesh/builder/bounding-box';
+import { PointRepresentation } from 'mol-geo/representation/structure/representation/point';
+import { StructureRepresentation } from 'mol-geo/representation/structure';
+import { BehaviorSubject } from 'rxjs';
+import { SpacefillRepresentation } from 'mol-geo/representation/structure/representation/spacefill';
+import { DistanceRestraintRepresentation } from 'mol-geo/representation/structure/representation/distance-restraint';
+
+export interface StructureView {
+    readonly viewer: Viewer
+
+    readonly label: string
+    readonly models: ReadonlyArray<Model>
+    readonly structure: Structure | undefined
+    readonly assemblySymmetry: AssemblySymmetry | undefined
+
+    readonly active: { [k: string]: boolean }
+    readonly structureRepresentations: { [k: string]: StructureRepresentation<any> }
+    readonly updated: BehaviorSubject<null>
+    readonly symmetryAxes: ShapeRepresentation<ShapeProps>
+
+    setSymmetryAxes(value: boolean): void
+    setStructureRepresentation(name: string, value: boolean): void
+
+    readonly modelId: number
+    readonly assemblyId: string
+    readonly symmetryFeatureId: number
+
+    setModel(modelId: number): Promise<void>
+    getModelIds(): { id: number, label: string }[]
+    setAssembly(assemblyId: string): Promise<void>
+    getAssemblyIds(): { id: string, label: string }[]
+    setSymmetryFeature(symmetryFeatureId: number): Promise<void>
+    getSymmetryFeatureIds(): { id: number, label: string }[]
+
+    destroy: () => void
+}
+
+interface StructureViewProps {
+    assemblyId?: string
+    symmetryFeatureId?: number
+}
+
+
+
+export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> {
+    const active: { [k: string]: boolean } = {
+        cartoon: true,
+        point: false,
+        ballAndStick: false,
+        carbohydrate: false,
+        spacefill: false,
+        distanceRestraint: false,
+        symmetryAxes: false,
+        // polymerSphere: false,
+    }
+
+    const structureRepresentations: { [k: string]: StructureRepresentation<any> } = {
+        cartoon: CartoonRepresentation(),
+        point: PointRepresentation(),
+        ballAndStick: BallAndStickRepresentation(),
+        carbohydrate: CarbohydrateRepresentation(),
+        spacefill: SpacefillRepresentation(),
+        distanceRestraint: DistanceRestraintRepresentation(),
+    }
+
+    const symmetryAxes = ShapeRepresentation()
+    const polymerSphere = ShapeRepresentation()
+
+    const updated: BehaviorSubject<null> = new BehaviorSubject<null>(null)
+
+    let label: string
+    let model: Model | undefined
+    let assemblySymmetry: AssemblySymmetry | undefined
+    let structure: Structure | undefined
+
+    let modelId: number
+    let assemblyId: string
+    let symmetryFeatureId: number
+
+    async function setSymmetryAxes(value: boolean) {
+        if (!value) {
+            assemblySymmetry = undefined
+        } else {
+            await AssemblySymmetry.attachFromCifOrAPI(models[modelId])
+            assemblySymmetry = AssemblySymmetry.get(models[modelId])
+        }
+        active.symmetryAxes = value
+        await setSymmetryFeature()
+    }
+
+    async function setStructureRepresentation(k: string, value: boolean) {
+        active[k] = value
+        await createStructureRepr()
+    }
+
+    async function setModel(newModelId: number, newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        console.log('setModel', newModelId)
+        modelId = newModelId
+        model = models[modelId]
+        if (active.symmetryAxes) {
+            await AssemblySymmetry.attachFromCifOrAPI(model)
+            assemblySymmetry = AssemblySymmetry.get(model)
+        }
+        await setAssembly(newAssemblyId, newSymmetryFeatureId)
+    }
+
+    function getModelIds() {
+        const modelIds: { id: number, label: string }[] = []
+        models.forEach((m, i) => {
+            modelIds.push({ id: i, label: `${i}: ${m.label} #${m.modelNum}` })
+        })
+        return modelIds
+    }
+
+    async function setAssembly(newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        console.log('setAssembly', newAssemblyId)
+        if (newAssemblyId !== undefined) {
+            assemblyId = newAssemblyId
+        } else if (model && model.symmetry.assemblies.length) {
+            assemblyId = model.symmetry.assemblies[0].id
+        } else if (model) {
+            assemblyId = '0'
+        } else {
+            assemblyId = '-1'
+        }
+        await getStructure()
+        await setSymmetryFeature(newSymmetryFeatureId)
+    }
+
+    function getAssemblyIds() {
+        const assemblyIds: { id: string, label: string }[] = [
+            { id: '0', label: '0: model' }
+        ]
+        if (model) model.symmetry.assemblies.forEach(a => {
+            assemblyIds.push({ id: a.id, label: `${a.id}: ${a.details}` })
+        })
+        return assemblyIds
+    }
+
+    async function setSymmetryFeature(newSymmetryFeatureId?: number) {
+        console.log('setSymmetryFeature', newSymmetryFeatureId)
+        if (newSymmetryFeatureId !== undefined) {
+            symmetryFeatureId = newSymmetryFeatureId
+        } else if (assemblySymmetry) {
+            const f = assemblySymmetry.getFeatures(assemblyId)
+            if (f._rowCount) {
+                symmetryFeatureId = f.id.value(0)
+            } else {
+                symmetryFeatureId = -1
+            }
+        } else {
+            symmetryFeatureId = -1
+        }
+        await createSymmetryRepr()
+    }
+
+    function getSymmetryFeatureIds() {
+        const symmetryFeatureIds: { id: number, label: string }[] = []
+        if (assemblySymmetry) {
+            const symmetryFeatures = assemblySymmetry.getFeatures(assemblyId)
+            for (let i = 0, il = symmetryFeatures._rowCount; i < il; ++i) {
+                const id = symmetryFeatures.id.value(i)
+                const symmetry = symmetryFeatures.symmetry_value.value(i)
+                const type = symmetryFeatures.type.value(i)
+                const stoichiometry = symmetryFeatures.stoichiometry_value.value(i)
+                const label = `${id}: ${symmetry} ${type} ${stoichiometry}`
+                symmetryFeatureIds.push({ id, label })
+            }
+        }
+        return symmetryFeatureIds
+    }
+
+    async function getStructure() {
+        if (model) structure = await getStructureFromModel(model, assemblyId)
+        if (model && structure) {
+            label = `${model.label} - Assembly ${assemblyId}`
+        } else {
+            label = ''
+        }
+        await createStructureRepr()
+    }
+
+    async function createStructureRepr() {
+        if (structure) {
+            console.log('createStructureRepr')
+            for (const k in structureRepresentations) {
+                if (active[k]) {
+                    await structureRepresentations[k].createOrUpdate({}, structure).run()
+                    viewer.add(structureRepresentations[k])
+                } else {
+                    viewer.remove(structureRepresentations[k])
+                }
+            }
+
+            viewer.center(structure.boundary.sphere.center)
+
+            // const mb = MeshBuilder.create()
+            // mb.setGroup(0)
+            // addSphere(mb, structure.boundary.sphere.center, structure.boundary.sphere.radius, 3)
+            // addBoundingBox(mb, structure.boundary.box, 1, 2, 8)
+            // for (let i = 0, il = structure.units.length; i < il; ++i) {
+            //     mb.setGroup(1)
+            //     const u = structure.units[i]
+            //     const ci = u.model.atomicHierarchy.chainAtomSegments.index[u.elements[0]]
+            //     const ek = u.model.atomicHierarchy.getEntityKey(ci)
+            //     if (u.model.entities.data.type.value(ek) === 'water') continue
+            //     const boundary = computeUnitBoundary(u)
+            //     addSphere(mb, boundary.sphere.center, boundary.sphere.radius, 3)
+            //     addBoundingBox(mb, boundary.box, 0.5, 2, 8)
+            // }
+            // const shape = Shape.create('boundary', mb.getMesh(), [Color(0xCC6633), Color(0x3366CC)], ['sphere boundary'])
+            // await polymerSphere.createOrUpdate({
+            //     alpha: 0.5,
+            //     doubleSided: false,
+            //     depthMask: false,
+            //     useFog: false // TODO fog not working properly
+            // }, shape).run()
+        } else {
+            for (const k in structureRepresentations) structureRepresentations[k].destroy()
+            polymerSphere.destroy()
+        }
+
+        viewer.add(polymerSphere)
+
+        updated.next(null)
+        viewer.requestDraw(true)
+        console.log(viewer.stats)
+    }
+
+    async function createSymmetryRepr() {
+        if (assemblySymmetry) {
+            const features = assemblySymmetry.getFeatures(assemblyId)
+            if (features._rowCount) {
+                const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry)
+                if (axesShape) {
+                    // const colorTheme = getClusterColorTheme(symmetryFeatureId, assemblySymmetry)
+                    // await cartoon.createOrUpdate({
+                    //     colorTheme: { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity },
+                    //     sizeTheme: { name: 'uniform', value: 0.2 },
+                    //     useFog: false // TODO fog not working properly
+                    // }).run()
+                    // await ballAndStick.createOrUpdate({
+                    //     colorTheme:  { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity },
+                    //     sizeTheme: { name: 'uniform', value: 0.1 },
+                    //     useFog: false // TODO fog not working properly
+                    // }).run()
+                    await symmetryAxes.createOrUpdate({}, axesShape).run()
+                    viewer.add(symmetryAxes)
+                } else {
+                    viewer.remove(symmetryAxes)
+                }
+            } else {
+                viewer.remove(symmetryAxes)
+            }
+        } else {
+            viewer.remove(symmetryAxes)
+        }
+        updated.next(null)
+        viewer.requestDraw(true)
+    }
+
+    await setModel(0, props.assemblyId, props.symmetryFeatureId)
+
+    return {
+        viewer,
+
+        get label() { return label },
+        models,
+        get structure() { return structure },
+        get assemblySymmetry() { return assemblySymmetry },
+
+        active,
+        structureRepresentations,
+        updated,
+        symmetryAxes,
+
+        setSymmetryAxes,
+        setStructureRepresentation,
+
+        get modelId() { return modelId },
+        get assemblyId() { return assemblyId },
+        get symmetryFeatureId() { return symmetryFeatureId },
+
+        setModel,
+        getModelIds,
+        setAssembly,
+        getAssemblyIds,
+        setSymmetryFeature,
+        getSymmetryFeatureIds,
+
+        destroy: () => {
+            for (const k in structureRepresentations) {
+                viewer.remove(structureRepresentations[k])
+                structureRepresentations[k].destroy()
+            }
+            viewer.remove(polymerSphere)
+            viewer.remove(symmetryAxes)
+            viewer.requestDraw(true)
+
+            polymerSphere.destroy()
+            symmetryAxes.destroy()
+        }
+    }
+}
+
+// // create new structure via query
+// const q1 = Q.generators.atoms({
+//     residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7
+// });
+// const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure));
+
+// // ball+stick for new structure
+// const newBallStickRepr = BallAndStickRepresentation()
+// await newBallStickRepr.create(newStructure, {
+//     colorTheme: { name: 'element-symbol' },
+//     sizeTheme: { name: 'uniform', value: 0.1 },
+//     useFog: false // TODO fog not working properly
+// }).run()
+// viewer.add(newBallStickRepr)
+
+// // create a mesh
+// const meshBuilder = MeshBuilder.create(256, 128)
+// const colors: Color[] = []
+// const labels: string[] = []
+// // red sphere
+// meshBuilder.setGroup(0)
+// colors[0] = Color(0xFF2233)
+// labels[0] = 'red sphere'
+// addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2)
+// // green cube
+// meshBuilder.setGroup(1)
+// colors[1] = Color(0x2233FF)
+// labels[1] = 'blue cube'
+// const t = Mat4.identity()
+// Mat4.fromTranslation(t, Vec3.create(10, 0, 0))
+// Mat4.scale(t, t, Vec3.create(3, 3, 3))
+// meshBuilder.add(t, Box())
+// const mesh = meshBuilder.getMesh()
+// const mesh = getObjFromUrl('mesh.obj')
+
+// // create shape from mesh
+// const shape = Shape.create('myShape', mesh, colors, labels)
+
+// // add representation from shape
+// const customRepr = ShapeRepresentation()
+// await customRepr.create(shape, {
+//     colorTheme: { name: 'shape-group' },
+//     // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
+//     useFog: false // TODO fog not working properly
+// }).run()
+// viewer.add(customRepr)

+ 46 - 0
src/apps/canvas/util.ts

@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import CIF, { CifBlock } from 'mol-io/reader/cif'
+import { readUrlAs, readFileAs } from 'mol-util/read';
+import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure';
+// import { parse as parseObj } from 'mol-io/reader/obj/parser'
+
+// export async function getObjFromUrl(url: string) {
+//     const data = await readUrlAs(url, false) as string
+//     const comp = parseObj(data)
+//     const parsed = await comp.run()
+//     if (parsed.isError) throw parsed
+//     return parsed.result
+// }
+
+export async function getCifFromData(data: string | Uint8Array) {
+    const comp = CIF.parse(data)
+    const parsed = await comp.run()
+    if (parsed.isError) throw parsed
+    return parsed.result.blocks[0]
+}
+
+export async function getCifFromUrl(url: string) {
+    return getCifFromData(await readUrlAs(url, false))
+}
+
+export async function getCifFromFile(file: File, binary = false) {
+    return getCifFromData(await readFileAs(file, binary))
+}
+
+export async function getModelsFromMmcif(cif: CifBlock) {
+    return await Model.create(Format.mmCIF(cif)).run()
+}
+
+export async function getStructureFromModel(model: Model, assembly: string) {
+    const assemblies = model.symmetry.assemblies
+    if (assembly === '0') {
+        return Structure.ofModel(model)
+    } else if (assemblies.find(a => a.id === assembly)) {
+        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
+    }
+}

+ 0 - 6
src/apps/viewer/index.tsx

@@ -104,10 +104,4 @@ ctx.dispatcher.getStream(InteractivityEvents.HighlightLoci).subscribe(event => {
     }
 })
 
-ctx.dispatcher.getStream(InteractivityEvents.SelectLoci).subscribe(event => {
-    if (event && event.data) {
-        ctx.stage.viewer.mark(event.data, MarkerAction.ToggleSelect)
-    }
-})
-
 ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm);

+ 14 - 9
src/mol-app/ui/visualization/viewport.tsx

@@ -163,19 +163,24 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl?
         viewer.input.move.subscribe(({x, y, inside, buttons}) => {
             if (!inside || buttons) return
             const p = viewer.identify(x, y)
-            const loci = viewer.getLoci(p)
-            InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci);
-
-            // TODO use LabelLoci event and make configurable
-            const label = labelFirst(loci)
-            const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}`
-            this.setState({ info })
+            if (p) {
+                const loci = viewer.getLoci(p)
+                InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci);
+
+                // TODO use LabelLoci event and make configurable
+                const label = labelFirst(loci)
+                const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}`
+                this.setState({ info })
+            }
         })
 
         // TODO filter only for left button/single finger touch?
         viewer.input.click.subscribe(({x, y}) => {
-            const loci = viewer.getLoci(viewer.identify(x, y))
-            InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci);
+            const p = viewer.identify(x, y)
+            if (p) {
+                const loci = viewer.getLoci(p)
+                InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci);
+            }
         })
     }
 

+ 15 - 0
src/mol-data/db/table.ts

@@ -101,6 +101,14 @@ namespace Table {
         return ret as Table<R>;
     }
 
+    export function pick<S extends R, R extends Schema>(table: Table<S>, schema: R, test: (i: number) => boolean) {
+        const _view: number[] = []
+        for (let i = 0, il = table._rowCount; i < il; ++i) {
+            if (test(i)) _view.push(i)
+        }
+        return view(table, schema, _view)
+    }
+
     export function window<S extends R, R extends Schema>(table: Table<S>, schema: R, start: number, end: number) {
         if (start === 0 && end === table._rowCount) return table;
         const ret = Object.create(null);
@@ -194,6 +202,13 @@ namespace Table {
         return row;
     }
 
+    /** Pick the first row for which `test` evaluates to true */
+    export function pickRow<S extends Schema>(table: Table<S>, test: (i: number) => boolean) {
+        for (let i = 0, il = table._rowCount; i < il; ++i) {
+            if (test(i)) return getRow(table, i)
+        }
+    }
+
     export function getRows<S extends Schema>(table: Table<S>) {
         const ret: Row<S>[] = [];
         const { _rowCount: c } = table;

+ 1 - 0
src/mol-data/int/impl/interval.ts

@@ -19,6 +19,7 @@ export function size(i: Tuple) { return Tuple.snd(i) - Tuple.fst(i); }
 export const hashCode = Tuple.hashCode;
 
 export function has(int: Tuple, v: number) { return Tuple.fst(int) <= v && v < Tuple.snd(int); }
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(int: Tuple, x: number) { const m = start(int); return x >= m && x < end(int) ? x - m : -1; }
 export function getAt(int: Tuple, i: number) { return Tuple.fst(int) + i; }
 

+ 1 - 0
src/mol-data/int/impl/ordered-set.ts

@@ -25,6 +25,7 @@ export function ofSortedArray(xs: Nums): OrderedSetImpl {
 
 export function size(set: OrderedSetImpl) { return I.is(set) ? I.size(set) : S.size(set); }
 export function has(set: OrderedSetImpl, x: number) { return I.is(set) ? I.has(set, x) : S.has(set, x); }
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(set: OrderedSetImpl, x: number) { return I.is(set) ? I.indexOf(set, x) : S.indexOf(set, x); }
 export function getAt(set: OrderedSetImpl, i: number) { return I.is(set) ? I.getAt(set, i) : set[i]; }
 export function min(set: OrderedSetImpl) { return I.is(set) ? I.min(set) : S.min(set); }

+ 1 - 0
src/mol-data/int/impl/sorted-array.ts

@@ -36,6 +36,7 @@ export function hashCode(xs: Nums) {
     return hash3(s, xs[0], xs[s - 1]);
 }
 
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(xs: Nums, v: number) {
     const l = xs.length;
     return l === 0 ? -1 : xs[0] <= v && v <= xs[l - 1] ? binarySearchRange(xs, v, 0, l) : -1;

+ 1 - 0
src/mol-data/int/interval.ts

@@ -18,6 +18,7 @@ namespace Interval {
 
     /** Test if a value is within the bounds of the interval */
     export const has: <T extends number = number>(interval: Interval<T>, x: T) => boolean = Impl.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(interval: Interval<T>, x: T) => number = Impl.indexOf as any;
     export const getAt: <T extends number = number>(interval: Interval<T>, i: number) => T = Impl.getAt as any;
 

+ 1 - 0
src/mol-data/int/ordered-set.ts

@@ -18,6 +18,7 @@ namespace OrderedSet {
     export const ofSortedArray: <T extends number = number>(xs: ArrayLike<T>) => OrderedSet<T> = Base.ofSortedArray as any;
 
     export const has: <T extends number = number>(set: OrderedSet<T>, x: T) => boolean = Base.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(set: OrderedSet<T>, x: T) => number = Base.indexOf as any;
     export const getAt: <T extends number = number>(set: OrderedSet<T>, i: number) => T = Base.getAt as any;
 

+ 1 - 0
src/mol-data/int/sorted-array.ts

@@ -19,6 +19,7 @@ namespace SortedArray {
     export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
 
     export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(array: SortedArray<T>, x: T) => number = Impl.indexOf as any;
     export const indexOfInInterval: <T extends number = number>(array: SortedArray<T>, x: number, bounds: Interval) => number = Impl.indexOfInInterval as any;
 

+ 1 - 1
src/mol-data/int/sorted-ranges.ts

@@ -18,7 +18,7 @@ namespace SortedRanges {
     export function max<T extends number = number>(ranges: SortedRanges<T>) { return ranges[ranges.length - 1] }
     export function size<T extends number = number>(ranges: SortedRanges<T>) {
         let size = 0
-        for(let i = 0, il = ranges.length; i < il; i += 2) {
+        for (let i = 0, il = ranges.length; i < il; i += 2) {
             size += ranges[i + 1] - ranges[i] + 1
         }
         return size

+ 14 - 1
src/mol-data/util/hash-functions.ts

@@ -1,7 +1,8 @@
 /**
- * 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>
  */
 
 // from http://burtleburtle.net/bob/hash/integer.html
@@ -67,4 +68,16 @@ export function cantorPairing(a: number, b: number) {
  */
 export function sortedCantorPairing(a: number, b: number) {
     return a < b ? cantorPairing(a, b) : cantorPairing(b, a);
+}
+
+/**
+ * 32 bit FNV-1a hash, see http://isthe.com/chongo/tech/comp/fnv/
+ */
+export function hashFnv32a(array: number[]) {
+    let hval = 0x811c9dc5;
+    for (let i = 0, il = array.length; i < il; ++i) {
+        hval ^= array[i];
+        hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
+    }
+    return hval >>> 0;
 }

+ 65 - 0
src/mol-geo/mesh/builder/bounding-box.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Box3D } from 'mol-math/geometry';
+import { MeshBuilder } from '../mesh-builder';
+import { CylinderProps } from '../../primitive/cylinder';
+import { addCylinder } from './cylinder';
+import { addSphere } from './sphere';
+
+const tmpStart = Vec3.zero()
+const tmpEnd = Vec3.zero()
+const cylinderProps: CylinderProps = {}
+
+export function addBoundingBox(builder: MeshBuilder, box: Box3D, radius: number, detail: number, radialSegments: number) {
+    const { min, max } = box
+
+    cylinderProps.radiusTop = radius
+    cylinderProps.radiusBottom = radius
+    cylinderProps.radialSegments = radialSegments
+
+    Vec3.set(tmpStart, max[0], max[1], max[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], min[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, min[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, max[0], min[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], min[1], max[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], max[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addSphere(builder, tmpEnd, radius, detail)
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addSphere(builder, tmpEnd, radius, detail)
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+}

+ 3 - 3
src/mol-geo/mesh/builder/tube.ts

@@ -84,7 +84,7 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>,
         Vec3.fromArray(u, normalVectors, offset)
         Vec3.fromArray(v, binormalVectors, offset)
         Vec3.fromArray(controlPoint, controlPoints, offset)
-        Vec3.cross(normalVector, u, v)
+        Vec3.cross(normalVector, v, u)
 
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
@@ -107,9 +107,9 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>,
 
             ChunkedArray.add3(
                 indices,
-                centerVertex,
+                vertexCount + (i + 1) % radialSegments,
                 vertexCount + i,
-                vertexCount + (i + 1) % radialSegments
+                centerVertex
             );
         }
     }

+ 6 - 7
src/mol-geo/representation/index.ts

@@ -13,20 +13,19 @@ import { MarkerAction } from '../util/marker-data';
 export interface RepresentationProps {}
 
 export interface Representation<D, P extends RepresentationProps = {}> {
+    readonly label: string
     readonly renderObjects: ReadonlyArray<RenderObject>
     readonly props: Readonly<P>
-    create: (data: D, props?: Partial<P>) => Task<void>
-    update: (props: Partial<P>) => Task<void>
+    createOrUpdate: (props?: Partial<P>, data?: D) => Task<void>
     getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => void
+    mark: (loci: Loci, action: MarkerAction) => boolean
     destroy: () => void
 }
 
 export interface Visual<D, P extends RepresentationProps = {}> {
-    readonly renderObject: RenderObject
-    create: (ctx: RuntimeContext, data: D, props?: Partial<P>) => Promise<void>
-    update: (ctx: RuntimeContext, props: Partial<P>) => Promise<boolean>
+    readonly renderObject: RenderObject | undefined
+    createOrUpdate: (ctx: RuntimeContext, props?: Partial<P>, data?: D) => Promise<void>
     getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => void
+    mark: (loci: Loci, action: MarkerAction) => boolean
     destroy: () => void
 }

+ 23 - 19
src/mol-geo/representation/shape/index.ts

@@ -10,7 +10,7 @@ import { RepresentationProps, Representation } from '..';
 import { PickingId } from '../../util/picking';
 import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data';
-import { createRenderableState, createMeshValues, createIdentityTransform, DefaultMeshProps } from '../util';
+import { createRenderableState, createMeshValues, DefaultMeshProps } from '../util';
 import { getMeshData } from '../../util/mesh-data';
 import { MeshValues } from 'mol-gl/renderable';
 import { ValueCell } from 'mol-util';
@@ -19,40 +19,48 @@ import { Shape } from 'mol-model/shape';
 import { LocationIterator } from '../../util/location-iterator';
 import { createColors } from '../structure/visual/util/common';
 import { OrderedSet, Interval } from 'mol-data/int';
+import { createIdentityTransform } from '../../util/transform-data';
 
 export interface ShapeRepresentation<P extends RepresentationProps = {}> extends Representation<Shape, P> { }
 
 export const DefaultShapeProps = {
     ...DefaultMeshProps,
+
     colorTheme: { name: 'shape-group' } as ColorThemeProps
 }
 export type ShapeProps = typeof DefaultShapeProps
 
+// TODO
+// export type ShapeRepresentation = ShapeRepresentation<ShapeProps>
+
 export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation<P> {
     const renderObjects: RenderObject[] = []
-    let _renderObject: MeshRenderObject
+    let _renderObject: MeshRenderObject | undefined
     let _shape: Shape
     let _props: P
 
-    function create(shape: Shape, props: Partial<P> = {}) {
+    function createOrUpdate(props: Partial<P> = {}, shape?: Shape) {
         _props = Object.assign({}, DefaultShapeProps, _props, props)
-        _shape = shape
+        if (shape) _shape = shape
 
         return Task.create('ShapeRepresentation.create', async ctx => {
             renderObjects.length = 0
 
-            const mesh = shape.mesh
-            const locationIt = ShapeGroupIterator.fromShape(shape)
+            if (!_shape) return
+
+            const mesh = _shape.mesh
+            const locationIt = ShapeGroupIterator.fromShape(_shape)
             const { groupCount, instanceCount } = locationIt
 
-            const color = createColors(locationIt, _props.colorTheme)
+            const transform = createIdentityTransform()
+            const color = await createColors(ctx, locationIt, _props.colorTheme)
             const marker = createMarkers(instanceCount * groupCount)
             const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
 
             const values: MeshValues = {
                 ...getMeshData(mesh),
                 ...createMeshValues(_props, counts),
-                aTransform: createIdentityTransform(),
+                ...transform,
                 ...color,
                 ...marker,
 
@@ -61,31 +69,24 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation
             const state = createRenderableState(_props)
 
             _renderObject = createMeshRenderObject(values, state)
-            console.log(_renderObject)
             renderObjects.push(_renderObject)
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('ShapeRepresentation.update', async ctx => {
-            // TODO handle general update
-            // TODO check shape.colors.ref.version
-        })
-    }
-
     return {
+        label: 'Shape mesh',
         get renderObjects () { return renderObjects },
         get props () { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci(pickingId: PickingId) {
             const { objectId, groupId } = pickingId
-            if (_renderObject.id === objectId) {
+            if (_renderObject && _renderObject.id === objectId) {
                 return Shape.Loci([ { shape: _shape, ids: OrderedSet.ofSingleton(groupId) } ])
             }
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!_renderObject) return false
             const { tMarker } = _renderObject.values
             let changed = false
             if (isEveryLoci(loci)) {
@@ -107,9 +108,12 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObjects.length = 0
+            _renderObject = undefined
         }
     }
 }

+ 16 - 43
src/mol-geo/representation/structure/complex-representation.ts

@@ -8,70 +8,43 @@
 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 { Loci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
-import { getQualityProps } from '../util';
-import { StructureProps, DefaultStructureProps, StructureRepresentation } from '.';
+import { StructureProps, StructureRepresentation } from '.';
 import { ComplexVisual } from './complex-visual';
 
-export function ComplexRepresentation<P extends StructureProps>(visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
-    let visual: ComplexVisual<P>
-
+export function ComplexRepresentation<P extends StructureProps>(label: string, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
+    let visual: ComplexVisual<P> | undefined
     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
+    function createOrUpdate(props: Partial<P> = {}, structure?: Structure) {
+        _props = Object.assign({}, _props, props)
 
         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
+            if (!visual) visual = visualCtor()
+            await visual.createOrUpdate(ctx, _props, 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
+        return visual ? visual.getLoci(pickingId) : EmptyLoci
     }
 
     function mark(loci: Loci, action: MarkerAction) {
-        visual.mark(loci, action)
+        return visual ? visual.mark(loci, action) : false
     }
 
     function destroy() {
-        visual.destroy()
+        if (visual) visual.destroy()
     }
 
     return {
-        get renderObjects() { return [ visual.renderObject ] },
+        label,
+        get renderObjects() {
+            return visual && visual.renderObject ? [ visual.renderObject ] : []
+        },
         get props() { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci,
         mark,
         destroy

+ 63 - 39
src/mol-geo/representation/structure/complex-visual.ts

@@ -15,7 +15,7 @@ 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 { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
 import { Interval } from 'mol-data/int';
 
@@ -39,62 +39,85 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe
     const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
     const updateState = MeshUpdateState.create()
 
-    let renderObject: MeshRenderObject
+    let renderObject: MeshRenderObject | undefined
     let currentProps: P
     let mesh: Mesh
     let currentStructure: Structure
     let locationIt: LocationIterator
+    let conformationHash: number
 
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) {
-            currentProps = Object.assign({}, defaultProps, props)
-            currentStructure = structure
+    async function create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) {
+        currentProps = Object.assign({}, defaultProps, props)
+        currentProps.colorTheme.structure = structure
+        currentStructure = structure
 
-            mesh = await createMesh(ctx, currentStructure, currentProps, mesh)
+        conformationHash = Structure.conformationHash(currentStructure)
+        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)
+        locationIt = createLocationIterator(structure)
+        renderObject = await createComplexMeshRenderObject(ctx, structure, mesh, locationIt, currentProps)
+    }
 
-            if (!renderObject) return false
+    async function update(ctx: RuntimeContext, props: Partial<P>) {
+        const newProps = Object.assign({}, currentProps, props)
+        newProps.colorTheme.structure = currentStructure
 
-            locationIt.reset()
-            MeshUpdateState.reset(updateState)
-            setUpdateState(updateState, newProps, currentProps)
+        if (!renderObject) return false
 
-            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
-                updateState.createMesh = true
-            }
+        locationIt.reset()
+        MeshUpdateState.reset(updateState)
+        setUpdateState(updateState, newProps, currentProps)
 
-            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
-                updateState.updateColor = true
-            }
+        const newConformationHash = Structure.conformationHash(currentStructure)
+        if (newConformationHash !== conformationHash) {
+            conformationHash = newConformationHash
+            updateState.createMesh = true
+        }
 
-            //
+        if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true
+        if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true
+        // if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true // TODO
 
-            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)
-            }
+        if (updateState.createMesh) {
+            mesh = await createMesh(ctx, currentStructure, newProps, mesh)
+            ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+            updateState.updateColor = true
+        }
+
+        if (updateState.updateColor) {
+            await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+        }
 
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
+        updateMeshValues(renderObject.values, newProps)
+        updateRenderableState(renderObject.state, newProps)
+
+        currentProps = newProps
+        return true
+    }
 
-            currentProps = newProps
-            return true
+    return {
+        get renderObject () { return renderObject },
+        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structure?: Structure) {
+            if (!structure && !currentStructure) {
+                throw new Error('missing structure')
+            } else if (structure && (!currentStructure || !renderObject)) {
+                await create(ctx, structure, props)
+            } else if (structure && structure.hashCode !== currentStructure.hashCode) {
+                await create(ctx, structure, props)
+            } else {
+                if (structure && Structure.conformationHash(structure) !== Structure.conformationHash(currentStructure)) {
+                    currentStructure = structure
+                }
+                await update(ctx, props)
+            }
         },
         getLoci(pickingId: PickingId) {
-            return getLoci(pickingId, currentStructure, renderObject.id)
+            return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
             const { tMarker } = renderObject.values
             const { groupCount, instanceCount } = locationIt
 
@@ -106,17 +129,18 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe
 
             let changed = false
             if (isEveryLoci(loci)) {
-                apply(Interval.ofBounds(0, groupCount * instanceCount))
-                changed = true
+                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, currentStructure, apply)
             }
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
 }

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

@@ -27,17 +27,20 @@ export const DefaultStructureMeshProps = {
 export type StructureMeshProps = typeof DefaultStructureMeshProps
 
 export interface MeshUpdateState {
+    updateTransform: boolean
     updateColor: boolean
     createMesh: boolean
 }
 export namespace MeshUpdateState {
     export function create(): MeshUpdateState {
         return {
+            updateTransform: false,
             updateColor: false,
             createMesh: false
         }
     }
     export function reset(state: MeshUpdateState) {
+        state.updateTransform = false
         state.updateColor = false
         state.createMesh = false
     }

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

@@ -11,40 +11,39 @@ 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 { getQualityProps } from '../../util';
 
 export const DefaultBackboneProps = {
     ...DefaultPolymerBackboneProps
 }
 export type BackboneProps = typeof DefaultBackboneProps
 
-export function BackboneRepresentation(): StructureRepresentation<BackboneProps> {
-    const traceRepr = UnitsRepresentation(PolymerBackboneVisual)
+export type BackboneRepresentation = StructureRepresentation<BackboneProps>
+
+export function BackboneRepresentation(): BackboneRepresentation {
+    const traceRepr = UnitsRepresentation('Polymer backbone cylinder', PolymerBackboneVisual)
 
     let currentProps: BackboneProps
     return {
+        label: 'Backbone',
         get renderObjects() {
             return [ ...traceRepr.renderObjects ]
         },
         get props() {
             return { ...traceRepr.props }
         },
-        create: (structure: Structure, props: Partial<BackboneProps> = {}) => {
-            currentProps = Object.assign({}, DefaultBackboneProps, props)
+        createOrUpdate: (props: Partial<BackboneProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultBackboneProps, currentProps, props, qualityProps)
             return Task.create('BackboneRepresentation', async ctx => {
-                await traceRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<BackboneProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating BackboneRepresentation', async ctx => {
-                await traceRepr.update(currentProps).runInContext(ctx)
+                await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
             return traceRepr.getLoci(pickingId)
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            traceRepr.mark(loci, action)
+            return traceRepr.mark(loci, action)
         },
         destroy() {
             traceRepr.destroy()

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

@@ -14,43 +14,40 @@ import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { InterUnitLinkVisual } from '../visual/inter-unit-link-cylinder';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
 export const DefaultBallAndStickProps = {
     ...DefaultElementSphereProps,
     ...DefaultIntraUnitLinkProps,
 
-    sizeTheme: { name: 'uniform', value: 0.25 } as SizeThemeProps,
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
     unitKinds: [ Unit.Kind.Atomic ] as Unit.Kind[]
 }
 export type BallAndStickProps = typeof DefaultBallAndStickProps
 
-export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> {
-    const elmementRepr = UnitsRepresentation(ElementSphereVisual)
-    const intraLinkRepr = UnitsRepresentation(IntraUnitLinkVisual)
-    const interLinkRepr = ComplexRepresentation(InterUnitLinkVisual)
+export type BallAndStickRepresentation = StructureRepresentation<BallAndStickProps>
+
+export function BallAndStickRepresentation(): BallAndStickRepresentation {
+    const elmementRepr = UnitsRepresentation('Element sphere mesh', ElementSphereVisual)
+    const intraLinkRepr = UnitsRepresentation('Intra-unit link cylinder', IntraUnitLinkVisual)
+    const interLinkRepr = ComplexRepresentation('Inter-unit link cylinder', InterUnitLinkVisual)
 
     let currentProps: BallAndStickProps
     return {
+        label: 'Ball & Stick',
         get renderObjects() {
             return [ ...elmementRepr.renderObjects, ...intraLinkRepr.renderObjects, ...interLinkRepr.renderObjects ]
         },
         get props() {
             return { ...elmementRepr.props, ...intraLinkRepr.props, ...interLinkRepr.props }
         },
-        create: (structure: Structure, props: Partial<BallAndStickProps> = {}) => {
-            currentProps = Object.assign({}, DefaultBallAndStickProps, props)
-            return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await elmementRepr.create(structure, currentProps).runInContext(ctx)
-                await intraLinkRepr.create(structure, currentProps).runInContext(ctx)
-                await interLinkRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<BallAndStickProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating BallAndStickRepresentation', async ctx => {
-                await elmementRepr.update(currentProps).runInContext(ctx)
-                await intraLinkRepr.update(currentProps).runInContext(ctx)
-                await interLinkRepr.update(currentProps).runInContext(ctx)
+        createOrUpdate: (props: Partial<BallAndStickProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultBallAndStickProps, currentProps, props, qualityProps)
+            return Task.create('BallAndStickRepresentation', async ctx => {
+                await elmementRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await intraLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await interLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
@@ -68,9 +65,10 @@ export function BallAndStickRepresentation(): StructureRepresentation<BallAndSti
             }
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            elmementRepr.mark(loci, action)
-            intraLinkRepr.mark(loci, action)
-            interLinkRepr.mark(loci, action)
+            const markElement = elmementRepr.mark(loci, action)
+            const markIntraLink = intraLinkRepr.mark(loci, action)
+            const markInterLink = interLinkRepr.mark(loci, action)
+            return markElement || markIntraLink || markInterLink
         },
         destroy() {
             elmementRepr.destroy()

+ 21 - 19
src/mol-geo/representation/structure/representation/carbohydrate.ts

@@ -12,37 +12,38 @@ 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';
+import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
-export const DefaultCartoonProps = {
+export const DefaultCarbohydrateProps = {
     ...DefaultCarbohydrateSymbolProps,
-    ...DefaultCarbohydrateLinkProps
+    ...DefaultCarbohydrateLinkProps,
+
+    sizeTheme: { name: 'uniform', value: 1, factor: 1 } as SizeThemeProps,
 }
-export type CarbohydrateProps = typeof DefaultCartoonProps
+export type CarbohydrateProps = typeof DefaultCarbohydrateProps
+
+export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateProps>
 
-export function CarbohydrateRepresentation(): StructureRepresentation<CarbohydrateProps> {
-    const carbohydrateSymbolRepr = ComplexRepresentation(CarbohydrateSymbolVisual)
-    const carbohydrateLinkRepr = ComplexRepresentation(CarbohydrateLinkVisual)
+export function CarbohydrateRepresentation(): CarbohydrateRepresentation {
+    const carbohydrateSymbolRepr = ComplexRepresentation('Carbohydrate symbol mesh', CarbohydrateSymbolVisual)
+    const carbohydrateLinkRepr = ComplexRepresentation('Carbohydrate link cylinder', CarbohydrateLinkVisual)
 
     let currentProps: CarbohydrateProps
     return {
+        label: 'Carbohydrate',
         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)
+        createOrUpdate: (props: Partial<CarbohydrateProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultCarbohydrateProps, currentProps, props, qualityProps)
             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)
+                await carbohydrateSymbolRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await carbohydrateLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
@@ -52,8 +53,9 @@ export function CarbohydrateRepresentation(): StructureRepresentation<Carbohydra
                 : carbohydrateLinkLoci
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            carbohydrateSymbolRepr.mark(loci, action)
-            carbohydrateLinkRepr.mark(loci, action)
+            const markSymbol = carbohydrateSymbolRepr.mark(loci, action)
+            const markLink = carbohydrateLinkRepr.mark(loci, action)
+            return markSymbol || markLink
         },
         destroy() {
             carbohydrateSymbolRepr.destroy()

+ 31 - 30
src/mol-geo/representation/structure/representation/cartoon.ts

@@ -13,24 +13,31 @@ 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 { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
+// import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge';
 
 export const DefaultCartoonProps = {
     ...DefaultPolymerTraceProps,
     ...DefaultPolymerGapProps,
     ...DefaultNucleotideBlockProps,
-    ...DefaultPolymerDirectionProps
+    // ...DefaultPolymerDirectionProps,
+
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
 }
 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)
+export type CartoonRepresentation = StructureRepresentation<CartoonProps>
+
+export function CartoonRepresentation(): CartoonRepresentation {
+    const traceRepr = UnitsRepresentation('Polymer trace mesh', PolymerTraceVisual)
+    const gapRepr = UnitsRepresentation('Polymer gap cylinder', PolymerGapVisual)
+    const blockRepr = UnitsRepresentation('Nucleotide block mesh', NucleotideBlockVisual)
+    // const directionRepr = UnitsRepresentation('Polymer direction wedge', PolymerDirectionVisual)
 
     let currentProps: CartoonProps
     return {
+        label: 'Cartoon',
         get renderObjects() {
             return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects,
                 ...blockRepr.renderObjects // , ...directionRepr.renderObjects
@@ -39,45 +46,39 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
         get props() {
             return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props }
         },
-        create: (structure: Structure, props: Partial<CartoonProps> = {}) => {
-            currentProps = Object.assign({}, DefaultCartoonProps, props)
+        createOrUpdate: (props: Partial<CartoonProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultCartoonProps, currentProps, props, qualityProps)
             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)
+                await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await gapRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await blockRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                // await directionRepr.createOrUpdate(currentProps, structure).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 directionLoci = directionRepr.getLoci(pickingId)
             return !isEmptyLoci(traceLoci) ? traceLoci
                 : !isEmptyLoci(gapLoci) ? gapLoci
-                : !isEmptyLoci(blockLoci) ? blockLoci
-                : directionLoci
+                : blockLoci
+                // : !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)
+            const markTrace = traceRepr.mark(loci, action)
+            const markGap = gapRepr.mark(loci, action)
+            const markBlock = blockRepr.mark(loci, action)
+            // const markDirection = directionRepr.mark(loci, action)
+            return markTrace || markGap || markBlock // \\ markDirection
         },
         destroy() {
             traceRepr.destroy()
             gapRepr.destroy()
             blockRepr.destroy()
-            directionRepr.destroy()
+            // directionRepr.destroy()
         }
     }
 }

+ 11 - 12
src/mol-geo/representation/structure/representation/distance-restraint.ts

@@ -12,6 +12,7 @@ import { Loci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from '../visual/cross-link-restraint-cylinder';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
 export const DefaultDistanceRestraintProps = {
     ...DefaultCrossLinkRestraintProps,
@@ -19,34 +20,32 @@ export const DefaultDistanceRestraintProps = {
 }
 export type DistanceRestraintProps = typeof DefaultDistanceRestraintProps
 
-export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> {
-    const crossLinkRepr = ComplexRepresentation(CrossLinkRestraintVisual)
+export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintProps>
+
+export function DistanceRestraintRepresentation(): DistanceRestraintRepresentation {
+    const crossLinkRepr = ComplexRepresentation('Cross-link restraint', CrossLinkRestraintVisual)
 
     let currentProps: DistanceRestraintProps
     return {
+        label: 'Distance restraint',
         get renderObjects() {
             return [ ...crossLinkRepr.renderObjects ]
         },
         get props() {
             return { ...crossLinkRepr.props }
         },
-        create: (structure: Structure, props: Partial<DistanceRestraintProps> = {}) => {
-            currentProps = Object.assign({}, DefaultDistanceRestraintProps, props)
+        createOrUpdate: (props: Partial<DistanceRestraintProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultDistanceRestraintProps, currentProps, props, qualityProps)
             return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<DistanceRestraintProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.update(currentProps).runInContext(ctx)
+                await crossLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
             return crossLinkRepr.getLoci(pickingId)
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            crossLinkRepr.mark(loci, action)
+            return crossLinkRepr.mark(loci, action)
         },
         destroy() {
             crossLinkRepr.destroy()

+ 47 - 0
src/mol-geo/representation/structure/representation/point.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 { UnitsRepresentation } from '..';
+import { ElementPointVisual, DefaultElementPointProps } from '../visual/element-point';
+import { StructureRepresentation } from '../units-representation';
+import { Structure } from 'mol-model/structure';
+import { MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { PickingId } from '../../../util/picking';
+
+export const DefaultPointProps = {
+    ...DefaultElementPointProps,
+}
+export type PointProps = typeof DefaultPointProps
+
+export type PointRepresentation = StructureRepresentation<PointProps>
+
+export function PointRepresentation(): PointRepresentation {
+    let currentProps: PointProps
+    const pointRepr = UnitsRepresentation('Point', ElementPointVisual)
+    return {
+        label: 'Point',
+        get renderObjects() {
+            return [ ...pointRepr.renderObjects ]
+        },
+        get props() {
+            return { ...pointRepr.props }
+        },
+        createOrUpdate: (props: Partial<PointProps> = {}, structure?: Structure) => {
+            currentProps = Object.assign({}, DefaultPointProps, currentProps, props)
+            return pointRepr.createOrUpdate(currentProps, structure)
+        },
+        getLoci: (pickingId: PickingId) => {
+            return pointRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            return pointRepr.mark(loci, action)
+        },
+        destroy() {
+            pointRepr.destroy()
+        }
+    }
+}

+ 34 - 2
src/mol-geo/representation/structure/representation/spacefill.ts

@@ -6,12 +6,44 @@
 
 import { UnitsRepresentation } from '..';
 import { ElementSphereVisual, DefaultElementSphereProps } from '../visual/element-sphere';
+import { StructureRepresentation } from '../units-representation';
+import { Structure } from 'mol-model/structure';
+import { PickingId } from '../../../util/picking';
+import { MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { getQualityProps } from '../../util';
 
 export const DefaultSpacefillProps = {
     ...DefaultElementSphereProps
 }
 export type SpacefillProps = typeof DefaultSpacefillProps
 
-export function SpacefillRepresentation() {
-    return UnitsRepresentation(ElementSphereVisual)
+export type SpacefillRepresentation = StructureRepresentation<SpacefillProps>
+
+export function SpacefillRepresentation(): SpacefillRepresentation {
+    let currentProps: SpacefillProps
+    const sphereRepr = UnitsRepresentation('Sphere mesh', ElementSphereVisual)
+    return {
+        label: 'Spacefill',
+        get renderObjects() {
+            return [ ...sphereRepr.renderObjects ]
+        },
+        get props() {
+            return { ...sphereRepr.props }
+        },
+        createOrUpdate: (props: Partial<SpacefillProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultSpacefillProps, currentProps, props, qualityProps)
+            return sphereRepr.createOrUpdate(currentProps, structure)
+        },
+        getLoci: (pickingId: PickingId) => {
+            return sphereRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            return sphereRepr.mark(loci, action)
+        },
+        destroy() {
+            sphereRepr.destroy()
+        }
+    }
 }

+ 78 - 60
src/mol-geo/representation/structure/units-representation.ts

@@ -12,87 +12,99 @@ 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 '.';
+import { StructureProps } from '.';
+import { StructureGroup } from './units-visual';
 
-export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, 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> {
+export function UnitsRepresentation<P extends StructureProps>(label: string, 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
+    function createOrUpdate(props: Partial<P> = {}, structure?: Structure) {
+        _props = Object.assign({}, _props, props)
 
-        return Task.create('Creating StructureRepresentation', async ctx => {
-            if (!_structure) {
+        return Task.create('Creating or updating StructureRepresentation', async ctx => {
+            if (!_structure && !structure) {
+                throw new Error('missing structure')
+            } else if (structure && !_structure) {
+                // console.log('initial structure')
+                // First call with a structure, create visuals for each group.
                 _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
                     const visual = visualCtor()
-                    await visual.create(ctx, group, _props)
+                    await visual.createOrUpdate(ctx, _props, { group, structure })
                     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 })
-                        }
+            } else if (structure && _structure.hashCode !== structure.hashCode) {
+                // console.log('_structure.hashCode !== structure.hashCode')
+                // Tries to re-use existing visuals for the groups of the new structure.
+                // Creates additional visuals if needed, destroys left-over visuals.
+                _groups = structure.unitSymmetryGroups;
+                // const newGroups: Unit.SymmetryGroup[] = []
+                const oldVisuals = visuals
+                visuals = new Map()
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visualGroup = oldVisuals.get(group.hashCode)
+                    if (visualGroup) {
+                        const { visual } = visualGroup
+                        await visual.createOrUpdate(ctx, _props, { group, structure })
+                        visuals.set(group.hashCode, { visual, group })
+                        oldVisuals.delete(group.hashCode)
+                    } else {
+                        // newGroups.push(group)
+                        const visual = visualCtor()
+                        await visual.createOrUpdate(ctx, _props, { group, structure })
+                        visuals.set(group.hashCode, { visual, group })
                     }
+                }
+                oldVisuals.forEach(({ visual }) => visual.destroy())
 
-                    // 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())
+                // TODO review logic
+                // For new groups, re-use left-over visuals
+                // const unusedVisuals: UnitsVisual<P>[] = []
+                // oldVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
+                // newGroups.forEach(async group => {
+                //     const visual = unusedVisuals.pop() || visualCtor()
+                //     await visual.createOrUpdate(ctx, _props, group)
+                //     visuals.set(group.hashCode, { visual, group })
+                // })
+                // unusedVisuals.forEach(visual => visual.destroy())
+            } else if (structure && _structure.hashCode === structure.hashCode) {
+                // console.log('_structure.hashCode === structure.hashCode')
+                // Expects that for structures with the same hashCode,
+                // the unitSymmetryGroups are the same as well.
+                // Re-uses existing visuals for the groups of the new structure.
+                _groups = structure.unitSymmetryGroups;
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visualGroup = visuals.get(group.hashCode)
+                    if (visualGroup) {
+                        await visualGroup.visual.createOrUpdate(ctx, _props, { group, structure })
+                        visualGroup.group = group
+                    } else {
+                        throw new Error(`expected to find visual for hashCode ${group.hashCode}`)
+                    }
                 }
+            } else {
+                // console.log('no new structure')
+                // No new structure given, just update all visuals with new props.
+                visuals.forEach(async ({ visual, group }) => {
+                    await visual.createOrUpdate(ctx, _props, { group, structure: _structure })
+                })
             }
-            _structure = structure
+            if (structure) _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 }) => {
@@ -103,7 +115,11 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () =>
     }
 
     function mark(loci: Loci, action: MarkerAction) {
-        visuals.forEach(({ visual }) => visual.mark(loci, action))
+        let changed = false
+        visuals.forEach(({ visual }) => {
+            changed = visual.mark(loci, action) || changed
+        })
+        return changed
     }
 
     function destroy() {
@@ -112,16 +128,18 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () =>
     }
 
     return {
+        label,
         get renderObjects() {
             const renderObjects: RenderObject[] = []
-            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
+            visuals.forEach(({ visual }) => {
+                if (visual.renderObject) renderObjects.push(visual.renderObject)
+            })
             return renderObjects
         },
         get props() {
             return _props
         },
-        create,
-        update,
+        createOrUpdate,
         getLoci,
         mark,
         destroy

+ 96 - 47
src/mol-geo/representation/structure/units-visual.ts

@@ -4,22 +4,25 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit } from 'mol-model/structure';
+import { Unit, Structure } 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 '../../util/location-iterator';
 import { Mesh } from '../../mesh/mesh';
-import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
-import { Loci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data';
+import { Loci, isEveryLoci, EmptyLoci } 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 { deepEqual, ValueCell, UUID } from 'mol-util';
 import { updateMeshValues, updateRenderableState } from '../util';
 import { Interval } from 'mol-data/int';
+import { createTransforms } from '../../util/transform-data';
 
-export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
+
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
 export const DefaultUnitsMeshProps = {
     ...DefaultStructureMeshProps,
@@ -40,66 +43,104 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
     const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
     const updateState = MeshUpdateState.create()
 
-    let renderObject: MeshRenderObject
+    let renderObject: MeshRenderObject | undefined
     let currentProps: P
     let mesh: Mesh
     let currentGroup: Unit.SymmetryGroup
+    let currentStructure: Structure
     let locationIt: LocationIterator
+    let currentConformationId: UUID
+
+    async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
+        currentProps = Object.assign({}, defaultProps, props)
+        currentProps.colorTheme.structure = currentStructure
+        currentGroup = group
+
+        const unit = group.units[0]
+        currentConformationId = Unit.conformationId(unit)
+        mesh = currentProps.unitKinds.includes(unit.kind)
+            ? await createMesh(ctx, unit, currentProps, mesh)
+            : Mesh.createEmpty(mesh)
+
+        // TODO create empty location iterator when not in unitKinds
+        locationIt = createLocationIterator(group)
+        renderObject = await createUnitsMeshRenderObject(ctx, group, mesh, locationIt, currentProps)
+    }
 
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
-            currentProps = Object.assign({}, defaultProps, props)
-            currentGroup = group
+    async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
+        if (!renderObject) return
 
-            const unit = group.units[0]
-            mesh = currentProps.unitKinds.includes(unit.kind)
-                ? await createMesh(ctx, unit, currentProps, mesh)
-                : Mesh.createEmpty(mesh)
+        const newProps = Object.assign({}, currentProps, props)
+        newProps.colorTheme.structure = currentStructure
+        const unit = currentGroup.units[0]
 
-            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]
+        locationIt.reset()
+        MeshUpdateState.reset(updateState)
+        setUpdateState(updateState, newProps, currentProps)
 
-            if (!renderObject) return false
+        const newConformationId = Unit.conformationId(unit)
+        if (newConformationId !== currentConformationId) {
+            currentConformationId = newConformationId
+            updateState.createMesh = true
+        }
 
-            locationIt.reset()
-            MeshUpdateState.reset(updateState)
-            setUpdateState(updateState, newProps, currentProps)
+        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
 
-            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
-                updateState.createMesh = true
-            }
+        if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true
+        if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true
+        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true
 
-            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
-                updateState.updateColor = true
-            }
+        //
 
-            //
+        if (updateState.updateTransform) {
+            locationIt = createLocationIterator(currentGroup)
+            const { instanceCount, groupCount } = locationIt
+            createTransforms(currentGroup, renderObject.values)
+            createMarkers(instanceCount * groupCount, renderObject.values)
+            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.createMesh) {
+            mesh = newProps.unitKinds.includes(unit.kind)
+                ? await createMesh(ctx, unit, newProps, mesh)
+                : Mesh.createEmpty(mesh)
+            ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+            updateState.updateColor = true
+        }
 
-            if (updateState.updateColor) {
-                createColors(locationIt, newProps.colorTheme, renderObject.values)
-            }
+        if (updateState.updateColor) {
+            await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+        }
 
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
+        updateMeshValues(renderObject.values, newProps)
+        updateRenderableState(renderObject.state, newProps)
 
-            currentProps = newProps
-            return true
+        currentProps = newProps
+    }
+
+    return {
+        get renderObject () { return renderObject },
+        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) {
+            if (structureGroup) currentStructure = structureGroup.structure
+            const group = structureGroup ? structureGroup.group : undefined
+            if (!group && !currentGroup) {
+                throw new Error('missing group')
+            } else if (group && (!currentGroup || !renderObject)) {
+                await create(ctx, group, props)
+            } else if (group && group.hashCode !== currentGroup.hashCode) {
+                await create(ctx, group, props)
+            } else {
+                if (group && !areGroupsIdentical(group, currentGroup)) {
+                    currentGroup = group
+                }
+                await update(ctx, props)
+            }
         },
         getLoci(pickingId: PickingId) {
-            return getLoci(pickingId, currentGroup, renderObject.id)
+            return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
             const { tMarker } = renderObject.values
             const { groupCount, instanceCount } = locationIt
 
@@ -111,17 +152,25 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
 
             let changed = false
             if (isEveryLoci(loci)) {
-                apply(Interval.ofBounds(0, groupCount * instanceCount))
-                changed = true
+                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, currentGroup, apply)
             }
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
+}
+
+function areGroupsIdentical(groupA: Unit.SymmetryGroup, groupB: Unit.SymmetryGroup) {
+    return (
+        groupA.units.length === groupB.units.length &&
+        Unit.conformationId(groupA.units[0]) === Unit.conformationId(groupB.units[0])
+    )
 }

+ 13 - 11
src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts

@@ -92,15 +92,15 @@ function CarbohydrateLinkIterator(structure: Structure): LocationIterator {
         const link = links[groupIndex]
         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)
+        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.indexOf(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(groupCount, instanceCount, getLocation)
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
@@ -110,14 +110,16 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
         const l = links[groupId]
         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
-            )
-        ])
+        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon)
+        if (indexA !== -1 && indexB !== -1) {
+            return Link.Loci([
+                Link.Location(
+                    carbA.unit, indexA as StructureElement.UnitIndex,
+                    carbB.unit, indexB as StructureElement.UnitIndex
+                )
+            ])
+        }
     }
     return EmptyLoci
 }

+ 10 - 6
src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts

@@ -5,7 +5,7 @@
  */
 
 import { Unit, Structure, StructureElement } from 'mol-model/structure';
-import { ComplexVisual } from '..';
+import { ComplexVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { PickingId } from '../../../util/picking';
@@ -154,7 +154,9 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolProp
         createLocationIterator: CarbohydrateElementIterator,
         getLoci: getCarbohydrateLoci,
         mark: markCarbohydrate,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: CarbohydrateSymbolProps, currentProps: CarbohydrateSymbolProps) => {
+            state.createMesh = newProps.detail !== currentProps.detail
+        }
     })
 }
 
@@ -172,7 +174,7 @@ function CarbohydrateElementIterator(structure: Structure): LocationIterator {
     function isSecondary (elementIndex: number, instanceIndex: number) {
         return (elementIndex % 2) === 1
     }
-    return LocationIterator(groupCount, instanceCount, getLocation, isSecondary)
+    return LocationIterator(groupCount, instanceCount, getLocation, true, isSecondary)
 }
 
 function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: number) {
@@ -180,9 +182,11 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     if (id === objectId) {
         const carb = structure.carbohydrates.elements[Math.floor(groupId / 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 }])
+        const index = OrderedSet.indexOf(unit.elements, carb.anomericCarbon)
+        if (index !== -1) {
+            const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex)
+            return StructureElement.Loci([{ unit, indices }])
+        }
     }
     return EmptyLoci
 }

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

@@ -84,7 +84,7 @@ function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
         location.bIndex = pair.indexB
         return location
     }
-    return LocationIterator(groupCount, instanceCount, getLocation)
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {

+ 127 - 94
src/mol-geo/representation/structure/visual/element-point.ts

@@ -6,131 +6,164 @@
  */
 
 import { ValueCell } from 'mol-util/value-cell'
-import { createPointRenderObject, PointRenderObject } from 'mol-gl/render-object'
-import { Unit } from 'mol-model/structure';
+import { PointRenderObject } from 'mol-gl/render-object'
+import { Unit, Structure } from 'mol-model/structure';
 import { RuntimeContext } from 'mol-task'
-
 import { UnitsVisual, DefaultStructureProps } from '..';
-import { getElementLoci, StructureElementIterator } from './util/element';
-import { createTransforms, createColors, createSizes } from './util/common';
-import { deepEqual, defaults } from 'mol-util';
-import { SortedArray } from 'mol-data/int';
-import { RenderableState, PointValues } from 'mol-gl/renderable';
+import { getElementLoci, StructureElementIterator, markElement } from './util/element';
+import { createColors, createSizes, createUnitsPointRenderObject } from './util/common';
+import { deepEqual, UUID } from 'mol-util';
+import { Interval } from 'mol-data/int';
 import { PickingId } from '../../../util/picking';
-import { Loci } from 'mol-model/loci';
-import { MarkerAction, createMarkers } from '../../../util/marker-data';
+import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, createMarkers, applyMarkerAction } from '../../../util/marker-data';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { fillSerial } from 'mol-util/array';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { LocationIterator } from '../../../util/location-iterator';
+import { createTransforms } from '../../../util/transform-data';
+import { StructureGroup } from '../units-visual';
+import { updateRenderableState } from '../../util';
 
-export const DefaultPointProps = {
+export const DefaultElementPointProps = {
     ...DefaultStructureProps,
-    sizeTheme: { name: 'physical' } as SizeThemeProps
+
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
+    pointSizeAttenuation: true,
 }
-export type PointProps = Partial<typeof DefaultPointProps>
+export type ElementPointProps = Partial<typeof DefaultElementPointProps>
 
-export function createPointVertices(unit: Unit) {
+export async function createElementPointVertices(ctx: RuntimeContext, unit: Unit, vertices?: ValueCell<Float32Array>) {
     const elements = unit.elements
-    const elementCount = elements.length
-    const vertices = new Float32Array(elementCount * 3)
+    const n = elements.length * 3
+    const array = vertices && vertices.ref.value.length >= n ? vertices.ref.value : new Float32Array(n)
 
     const pos = unit.conformation.invariantPosition
 
     const p = Vec3.zero()
-    for (let i = 0; i < elementCount; i++) {
-        const i3 = i * 3
-        pos(elements[i], p)
-        vertices[i3] = p[0]
-        vertices[i3 + 1] = p[1]
-        vertices[i3 + 2] = p[2]
+    for (let i = 0; i < n; i += 3) {
+        pos(elements[i / 3], p)
+        array[i] = p[0]
+        array[i + 1] = p[1]
+        array[i + 2] = p[2]
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating points', current: i / 3, max: elements.length });
+        }
+        ++i
     }
-    return vertices
+    return vertices ? ValueCell.update(vertices, array) : ValueCell.create(array)
 }
 
-export default function PointVisual(): UnitsVisual<PointProps> {
-    let renderObject: PointRenderObject
-    let currentProps = DefaultPointProps
+export function ElementPointVisual(): UnitsVisual<ElementPointProps> {
+    let renderObject: PointRenderObject | undefined
+    let currentProps = DefaultElementPointProps
     let currentGroup: Unit.SymmetryGroup
-
-    let _units: ReadonlyArray<Unit>
-    let _elements: SortedArray
+    let currentStructure: Structure
+    let locationIt: LocationIterator
+    let vertices: ValueCell<Float32Array>
+    let currentConformationId: UUID
 
     return {
         get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PointProps = {}) {
-            currentProps = Object.assign({}, DefaultPointProps, props)
-            currentGroup = group
-
-            _units = group.units
-            _elements = group.elements;
-
-            const { colorTheme, sizeTheme } = currentProps
-            const elementCount = _elements.length
-            const instanceCount = group.units.length
-
-            const locationIt = StructureElementIterator.fromGroup(group)
-
-            const vertices = createPointVertices(_units[0])
-            const transforms = createTransforms(group)
-            const color = createColors(locationIt, colorTheme)
-            const size = createSizes(locationIt, sizeTheme)
-            const marker = createMarkers(instanceCount * elementCount)
-
-            const values: PointValues = {
-                aPosition: ValueCell.create(vertices),
-                aGroup: ValueCell.create(fillSerial(new Float32Array(elementCount))),
-                aTransform: transforms,
-                aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
-                ...color,
-                ...marker,
-                ...size,
-
-                uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
-                uInstanceCount: ValueCell.create(instanceCount),
-                uGroupCount: ValueCell.create(group.elements.length),
-
-                drawCount: ValueCell.create(vertices.length / 3),
-                instanceCount: ValueCell.create(instanceCount),
-
-                dPointSizeAttenuation: ValueCell.create(true),
-                dUseFog: ValueCell.create(defaults(props.useFog, true)),
-            }
-            const state: RenderableState = {
-                depthMask: defaults(props.depthMask, true),
-                visible: defaults(props.visible, true)
+        async createOrUpdate(ctx: RuntimeContext, props: ElementPointProps = {}, structureGroup?: StructureGroup) {
+            if (structureGroup) currentStructure = structureGroup.structure
+            const group = structureGroup ? structureGroup.group : undefined
+            if (!group && !currentGroup) {
+                throw new Error('missing group')
+            } else if (group && !currentGroup) {
+                currentProps = Object.assign({}, DefaultElementPointProps, props)
+                currentProps.colorTheme.structure = currentStructure
+                currentGroup = group
+                locationIt = StructureElementIterator.fromGroup(group)
+
+                const unit = group.units[0]
+                currentConformationId = Unit.conformationId(unit)
+                vertices = await createElementPointVertices(ctx, unit, vertices)
+
+                renderObject = await createUnitsPointRenderObject(ctx, group, vertices, locationIt, currentProps)
+            } else if (renderObject) {
+                if (group) currentGroup = group
+
+                const newProps = { ...currentProps, ...props }
+                const unit = currentGroup.units[0]
+
+                let updateTransform = false
+                let createVertices = false
+                let updateColor = false
+                let updateSize = false
+
+                const newConformationId = Unit.conformationId(unit)
+                if (newConformationId !== currentConformationId) {
+                    currentConformationId = newConformationId
+                    createVertices = true
+                }
+
+                if (currentGroup.units.length !== locationIt.instanceCount) updateTransform = true
+
+                if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) createVertices = true
+                if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateColor = true
+                if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateSize = true
+
+                if (updateTransform) {
+                    locationIt = StructureElementIterator.fromGroup(currentGroup)
+                    const { instanceCount, groupCount } = locationIt
+                    createTransforms(currentGroup, renderObject.values)
+                    createMarkers(instanceCount * groupCount, renderObject.values)
+                    updateColor = true
+                    updateSize = true
+                }
+
+                if (createVertices) {
+                    await createElementPointVertices(ctx, unit, vertices)
+                    ValueCell.update(renderObject.values.aGroup, fillSerial(new Float32Array(locationIt.groupCount))) // TODO reuse array
+                    ValueCell.update(renderObject.values.drawCount, locationIt.groupCount)
+                    updateColor = true
+                    updateSize = true
+                }
+
+                if (updateColor) {
+                    await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+                }
+
+                if (updateSize) {
+                    await createSizes(ctx, locationIt, newProps.sizeTheme, renderObject.values)
+                }
+
+                updateRenderableState(renderObject.state, newProps)
+
+                currentProps = newProps
             }
-
-            renderObject = createPointRenderObject(values, state)
         },
-        async update(ctx: RuntimeContext, props: PointProps) {
-            if (!renderObject || !_units || !_elements) return false
-
-            const newProps = { ...currentProps, ...props }
-            if (deepEqual(currentProps, newProps)) {
-                console.log('props identical, nothing to change')
-                return true
+        getLoci(pickingId: PickingId) {
+            return renderObject ? getElementLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
+            const { tMarker } = renderObject.values
+            const { groupCount, 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)
             }
 
-            if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) {
-                console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme)
+            let changed = false
+            if (isEveryLoci(loci)) {
+                apply(Interval.ofBounds(0, groupCount * instanceCount))
+                changed = true
+            } else {
+                changed = markElement(loci, currentGroup, apply)
             }
-
-            if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) {
-                console.log('sizeTheme changed', currentProps.sizeTheme, newProps.sizeTheme)
+            if (changed) {
+                ValueCell.update(tMarker, tMarker.ref.value)
             }
-
-            currentProps = newProps
-            return false
-        },
-        getLoci(pickingId: PickingId) {
-            return getElementLoci(pickingId, currentGroup, renderObject.id)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            // TODO
-            // markElement(loci, action, currentGroup, renderObject.values)
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
 }

+ 1 - 0
src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts

@@ -51,6 +51,7 @@ async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: S
 export const DefaultInterUnitLinkProps = {
     ...DefaultComplexMeshProps,
     ...DefaultLinkCylinderProps,
+
     sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
 export type InterUnitLinkProps = typeof DefaultInterUnitLinkProps

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

@@ -6,7 +6,7 @@
  */
 
 import { Unit, Link, StructureElement } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh, LinkIterator } from './util/link';
 import { Mesh } from '../../../mesh/mesh';
@@ -65,6 +65,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit,
 export const DefaultIntraUnitLinkProps = {
     ...DefaultUnitsMeshProps,
     ...DefaultLinkCylinderProps,
+
     sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
 export type IntraUnitLinkProps = typeof DefaultIntraUnitLinkProps
@@ -76,7 +77,9 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
         createLocationIterator: LinkIterator.fromGroup,
         getLoci: getLinkLoci,
         mark: markLink,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: LinkCylinderProps, currentProps: LinkCylinderProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }
 
@@ -99,12 +102,13 @@ function markLink(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Inter
 
     let changed = false
     if (Unit.isAtomic(unit) && Link.isLoci(loci)) {
+        const groupCount = unit.links.edgeCount * 2
         for (const b of loci.links) {
-            const unitIdx = Unit.findUnitById(b.aUnit.id, group.units)
-            if (unitIdx !== -1) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
                 const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
                 if (idx !== -1) {
-                    if (apply(Interval.ofSingleton(idx))) changed = true
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
                 }
             }
         }

+ 23 - 20
src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts

@@ -9,14 +9,14 @@ import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getElementLoci, markElement, StructureElementIterator } from './util/element';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
-import { Segmentation, SortedArray } from 'mol-data/int';
+import { Segmentation } from 'mol-data/int';
 import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
 import { getElementIndexForAtomId, getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
 import { addCylinder } from '../../../mesh/builder/cylinder';
 import { Box } from '../../../primitive/box';
+import { NucleotideLocationIterator, markNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
 
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
@@ -82,25 +82,28 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, props:
                     idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
                 }
 
-                if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1 && idx5 !== -1 && idx6 !== -1) {
-                    pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); pos(idx5, p5); pos(idx6, p6)
-                    Vec3.normalize(v12, Vec3.sub(v12, p2, p1))
-                    Vec3.normalize(v34, Vec3.sub(v34, p4, p3))
-                    Vec3.normalize(vC, Vec3.cross(vC, v12, v34))
-                    Mat4.targetTo(t, p1, p2, vC)
-                    Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2)
-                    Mat4.scale(t, t, Vec3.set(sVec, width, depth, height))
-                    Mat4.setTranslation(t, center)
-                    builder.setGroup(SortedArray.findPredecessorIndex(elements, idx6))
-                    builder.add(t, box)
+                if (idx5 !== -1 && idx6 !== -1) {
+                    pos(idx5, p5); pos(idx6, p6)
+                    builder.setGroup(i)
                     addCylinder(builder, p5, p6, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+                    if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1) {
+                        pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4);
+                        Vec3.normalize(v12, Vec3.sub(v12, p2, p1))
+                        Vec3.normalize(v34, Vec3.sub(v34, p4, p3))
+                        Vec3.normalize(vC, Vec3.cross(vC, v12, v34))
+                        Mat4.targetTo(t, p1, p2, vC)
+                        Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2)
+                        Mat4.scale(t, t, Vec3.set(sVec, width, depth, height))
+                        Mat4.setTranslation(t, center)
+                        builder.add(t, box)
+                    }
                 }
-            }
 
-            if (i % 10000 === 0 && ctx.shouldUpdate) {
-                await ctx.update({ message: 'Nucleotide block mesh', current: i });
+                if (i % 10000 === 0 && ctx.shouldUpdate) {
+                    await ctx.update({ message: 'Nucleotide block mesh', current: i });
+                }
+                ++i
             }
-            ++i
         }
     }
 
@@ -116,9 +119,9 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> {
     return UnitsMeshVisual<NucleotideBlockProps>({
         defaultProps: DefaultNucleotideBlockProps,
         createMesh: createNucleotideBlockMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: NucleotideLocationIterator.fromGroup,
+        getLoci: getNucleotideElementLoci,
+        mark: markNucleotideElement,
         setUpdateState: () => {}
     })
 }

+ 9 - 6
src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts

@@ -5,11 +5,11 @@
  */
 
 import { Unit } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer';
+import { PolymerBackboneIterator } from './util/polymer';
 import { getElementLoci, markElement, StructureElementIterator } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
@@ -24,7 +24,7 @@ export interface PolymerBackboneCylinderProps {
 }
 
 async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerBackboneCylinderProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -47,11 +47,11 @@ async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit
         pos(centerB.element, pB)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA)
-        builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerA.element))
+        builder.setGroup(OrderedSet.indexOf(elements, centerA.element))
         addCylinder(builder, pA, pB, 0.5, cylinderProps)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB)
-        builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerB.element))
+        builder.setGroup(OrderedSet.indexOf(elements, centerB.element))
         addCylinder(builder, pB, pA, 0.5, cylinderProps)
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
@@ -73,9 +73,12 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneProps> {
     return UnitsMeshVisual<PolymerBackboneProps>({
         defaultProps: DefaultPolymerBackboneProps,
         createMesh: createPolymerBackboneCylinderMesh,
+        // TODO create a specialized location iterator
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
         mark: markElement,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: PolymerBackboneProps, currentProps: PolymerBackboneProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }

+ 6 - 8
src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts

@@ -7,15 +7,13 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci, StructureElementIterator } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
-import { OrderedSet } from 'mol-data/int';
 import { Wedge } from '../../../primitive/wedge';
 
 const t = Mat4.identity()
@@ -35,7 +33,7 @@ export interface PolymerDirectionWedgeProps {
 }
 
 async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, props: PolymerDirectionWedgeProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -51,7 +49,7 @@ async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit,
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element))
+        builder.setGroup(i)
 
         const isNucleic = v.moleculeType === MoleculeType.DNA || v.moleculeType === MoleculeType.RNA
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -95,9 +93,9 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> {
     return UnitsMeshVisual<PolymerDirectionProps>({
         defaultProps: DefaultPolymerDirectionProps,
         createMesh: createPolymerDirectionWedgeMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: PolymerLocationIterator.fromGroup,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: () => {}
     })
 }

+ 19 - 24
src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts

@@ -4,13 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, StructureElement } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { Unit } from 'mol-model/structure';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerGapCount, PolymerGapIterator } from './util/polymer';
-import { getElementLoci, markElement, StructureElementIterator } from './util/element';
+import { PolymerGapIterator, PolymerGapLocationIterator, markPolymerGapElement, getPolymerGapElementLoci } from './util/polymer';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
@@ -26,7 +25,7 @@ export interface PolymerGapCylinderProps {
 }
 
 async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerGapCylinderProps, mesh?: Mesh) {
-    const polymerGapCount = getPolymerGapCount(unit)
+    const polymerGapCount = unit.gapElements.length
     if (!polymerGapCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -35,11 +34,9 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro
     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
     }
@@ -49,30 +46,26 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro
     while (polymerGapIt.hasNext) {
         const { centerA, centerB } = polymerGapIt.move()
         if (centerA.element === centerB.element) {
-            builder.setGroup(centerA.element)
-            pos(elements[centerA.element], pA)
+            builder.setGroup(i)
+            pos(centerA.element, pA)
             addSphere(builder, pA, 0.6, 0)
         } else {
-            const elmA = elements[centerA.element]
-            const elmB = elements[centerB.element]
-            pos(elmA, pA)
-            pos(elmB, pB)
+            pos(centerA.element, pA)
+            pos(centerB.element, pB)
 
-            l.element = elmA
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
-            builder.setGroup(centerA.element)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA)
+            builder.setGroup(i)
             addFixedCountDashedCylinder(builder, pA, pB, 0.5, segmentCount, cylinderProps)
 
-            l.element = elmB
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
-            builder.setGroup(centerB.element)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB)
+            builder.setGroup(i + 1)
             addFixedCountDashedCylinder(builder, pB, pA, 0.5, segmentCount, cylinderProps)
         }
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
             await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount });
         }
-        ++i
+        i += 2
     }
 
     return builder.getMesh()
@@ -88,9 +81,11 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> {
     return UnitsMeshVisual<PolymerGapProps>({
         defaultProps: DefaultPolymerGapProps,
         createMesh: createPolymerGapCylinderMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
-        setUpdateState: () => {}
+        createLocationIterator: PolymerGapLocationIterator.fromGroup,
+        getLoci: getPolymerGapElementLoci,
+        mark: markPolymerGapElement,
+        setUpdateState: (state: MeshUpdateState, newProps: PolymerGapProps, currentProps: PolymerGapProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }

+ 6 - 8
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -7,14 +7,12 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci, StructureElementIterator } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
-import { OrderedSet } from 'mol-data/int';
 import { addSheet } from '../../../mesh/builder/sheet';
 import { addTube } from '../../../mesh/builder/tube';
 
@@ -29,7 +27,7 @@ export interface PolymerTraceMeshProps {
 // TODO handle polymer ends properly
 
 async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: PolymerTraceMeshProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -45,7 +43,7 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: Po
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element))
+        builder.setGroup(i)
 
         const isNucleicType = isNucleic(v.moleculeType)
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -95,9 +93,9 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> {
     return UnitsMeshVisual<PolymerTraceProps>({
         defaultProps: DefaultPolymerTraceProps,
         createMesh: createPolymerTraceMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: PolymerLocationIterator.fromGroup,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: (state: MeshUpdateState, newProps: PolymerTraceProps, currentProps: PolymerTraceProps) => {
             state.createMesh = (
                 newProps.linearSegments !== currentProps.linearSegments ||

+ 74 - 40
src/mol-geo/representation/structure/visual/util/common.ts

@@ -6,57 +6,57 @@
  */
 
 import { Unit, Structure } from 'mol-model/structure';
-import { Mat4 } from 'mol-math/linear-algebra'
-
-import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor } from '../../../../util/color-data';
-import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize } from '../../../../util/size-data';
+import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor, ColorType } from '../../../../util/color-data';
+import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize, SizeType } from '../../../../util/size-data';
 import { ValueCell } from 'mol-util';
 import { LocationIterator } from '../../../../util/location-iterator';
 import { Mesh } from '../../../../mesh/mesh';
-import { MeshValues } from 'mol-gl/renderable';
+import { MeshValues, PointValues } from 'mol-gl/renderable';
 import { getMeshData } from '../../../../util/mesh-data';
-import { MeshProps, createMeshValues, createRenderableState, createIdentityTransform } from '../../../util';
+import { MeshProps, createMeshValues, createRenderableState, createPointValues } from '../../../util';
 import { StructureProps } from '../..';
 import { createMarkers } from '../../../../util/marker-data';
-import { createMeshRenderObject } from 'mol-gl/render-object';
+import { createMeshRenderObject, createPointRenderObject } from 'mol-gl/render-object';
 import { ColorThemeProps, ColorTheme } from 'mol-view/theme/color';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { RuntimeContext } from 'mol-task';
+import { PointProps } from 'mol-geo/representation/structure/representation/point';
+import { fillSerial } from 'mol-util/array';
+import { TransformData, createIdentityTransform, createTransforms } from '../../../../util/transform-data';
 
-export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
-    const unitCount = units.length
-    const n = unitCount * 16
-    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
-    for (let i = 0; i < unitCount; i++) {
-        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
-    }
-    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
+function getGranularity(locationIt: LocationIterator, granularity: ColorType | SizeType) {
+    // Always use 'group' granularity for 'complex' location iterators,
+    // i.e. for which an instance may include multiple units
+    return granularity === 'instance' && locationIt.isComplex ? 'group' : granularity
 }
 
-export function createColors(locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData) {
+export function createColors(ctx: RuntimeContext, locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData): Promise<ColorData> {
     const colorTheme = ColorTheme(props)
-    switch (colorTheme.kind) {
-        case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData)
-        case 'group': return createGroupColor(locationIt, colorTheme.color, colorData)
-        case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData)
-        case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData)
+    switch (getGranularity(locationIt, colorTheme.granularity)) {
+        case 'uniform': return createUniformColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'group': return createGroupColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'groupInstance': return createGroupInstanceColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'instance': return createInstanceColor(ctx, locationIt, colorTheme.color, colorData)
     }
 }
 
-export function createSizes(locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): SizeData {
+export async function createSizes(ctx: RuntimeContext, locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): Promise<SizeData> {
     const sizeTheme = SizeTheme(props)
-    switch (sizeTheme.kind) {
-        case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData)
-        case 'group': return createGroupSize(locationIt, sizeTheme.size, sizeData)
-        case 'groupInstance': return createGroupInstanceSize(locationIt, sizeTheme.size, sizeData)
-        case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData)
+    switch (getGranularity(locationIt, sizeTheme.granularity)) {
+        case 'uniform': return createUniformSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'group': return createGroupSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'groupInstance': return createGroupInstanceSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'instance': return createInstanceSize(ctx, locationIt, sizeTheme.size, sizeData)
     }
 }
 
+// mesh
+
 type StructureMeshProps = Required<MeshProps & StructureProps>
 
-function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+async function _createMeshValues(ctx: RuntimeContext, transforms: TransformData, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const { instanceCount, groupCount } = locationIt
-    const color = createColors(locationIt, props.colorTheme)
+    const color = await createColors(ctx, locationIt, props.colorTheme)
     const marker = createMarkers(instanceCount * groupCount)
 
     const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
@@ -65,35 +65,69 @@ function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, loca
         ...getMeshData(mesh),
         ...color,
         ...marker,
-        aTransform: transforms,
+        ...transforms,
         elements: mesh.indexBuffer,
         ...createMeshValues(props, counts)
     }
 }
 
-export function createComplexMeshValues(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function createComplexMeshValues(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createIdentityTransform()
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
 }
 
-export function createUnitsMeshValues(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function createUnitsMeshValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createTransforms(group)
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
 }
 
-export function createComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
-    const values = createComplexMeshValues(structure, mesh, locationIt, props)
+export async function createComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = await createComplexMeshValues(ctx, 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)
+export async function createUnitsMeshRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = await createUnitsMeshValues(ctx, group, mesh, locationIt, props)
     const state = createRenderableState(props)
     return createMeshRenderObject(values, state)
 }
 
-export function updateComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function updateComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createIdentityTransform()
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
+}
+
+// point
+
+type StructurePointProps = Required<PointProps & StructureProps>
+
+async function _createPointValues(ctx: RuntimeContext, transforms: TransformData, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> {
+    const { instanceCount, groupCount } = locationIt
+    const color = await createColors(ctx, locationIt, props.colorTheme)
+    const size = await createSizes(ctx, locationIt, props.sizeTheme)
+    const marker = createMarkers(instanceCount * groupCount)
+
+    const counts = { drawCount: groupCount, groupCount, instanceCount }
+
+    return {
+        aPosition: vertices,
+        aGroup: ValueCell.create(fillSerial(new Float32Array(groupCount))),
+        ...color,
+        ...size,
+        ...marker,
+        ...transforms,
+        ...createPointValues(props, counts)
+    }
+}
+
+export async function createUnitsPointValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> {
+    const transforms = createTransforms(group)
+    return _createPointValues(ctx, transforms, vertices, locationIt, props)
+}
+
+export async function createUnitsPointRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps) {
+    const values = await createUnitsPointValues(ctx, group, vertices, locationIt, props)
+    const state = createRenderableState(props)
+    return createPointRenderObject(values, state)
 }

+ 6 - 5
src/mol-geo/representation/structure/visual/util/element.ts

@@ -57,8 +57,8 @@ export function markElement(loci: Loci, group: Unit.SymmetryGroup, apply: (inter
     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) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
                 if (Interval.is(e.indices)) {
                     const start = unitIdx * elementCount + Interval.start(e.indices);
                     const end = unitIdx * elementCount + Interval.end(e.indices);
@@ -87,11 +87,12 @@ export function getElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup,
 
 export namespace StructureElementIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
-        const unit = group.units[0]
         const groupCount = group.elements.length
         const instanceCount = group.units.length
-        const location = StructureElement.create(unit)
-        const getLocation = (groupIndex: number) => {
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
             location.element = unit.elements[groupIndex]
             return location
         }

+ 5 - 3
src/mol-geo/representation/structure/visual/util/link.ts

@@ -126,8 +126,10 @@ export namespace LinkIterator {
         const unit = group.units[0]
         const groupCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
         const instanceCount = group.units.length
-        const location = StructureElement.create(unit)
-        const getLocation = (groupIndex: number) => {
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
             location.element = unit.elements[(unit as Unit.Atomic).links.a[groupIndex]]
             return location
         }
@@ -146,6 +148,6 @@ export namespace LinkIterator {
             location.bIndex = bond.indexB as StructureElement.UnitIndex
             return location
         }
-        return LocationIterator(groupCount, instanceCount, getLocation)
+        return LocationIterator(groupCount, instanceCount, getLocation, true)
     }
 }

+ 72 - 0
src/mol-geo/representation/structure/visual/util/nucleotide.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, StructureElement } from 'mol-model/structure';
+import { LocationIterator } from '../../../../util/location-iterator';
+import { getNucleotideElements } from 'mol-model/structure/structure/util/nucleotide';
+import { PickingId } from '../../../../util/picking';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { OrderedSet, Interval } from 'mol-data/int';
+
+export namespace NucleotideLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const u = group.units[0]
+        const nucleotideElementIndices = Unit.isAtomic(u) ? getNucleotideElements(u) : []
+        const groupCount = nucleotideElementIndices.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = nucleotideElementIndices[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export function getNucleotideElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        if (Unit.isAtomic(unit)) {
+            const unitIndex = OrderedSet.indexOf(unit.elements, unit.nucleotideElements[groupId]) as StructureElement.UnitIndex
+            if (unitIndex !== -1) {
+                const indices = OrderedSet.ofSingleton(unitIndex)
+                return StructureElement.Loci([{ unit, indices }])
+            }
+        }
+    }
+    return EmptyLoci
+}
+
+export function markNucleotideElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    const u = group.units[0]
+    if (StructureElement.isLoci(loci) && Unit.isAtomic(u)) {
+        const groupCount = u.nucleotideElements.length
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined && Unit.isAtomic(e.unit)) {
+                if (Interval.is(e.indices)) {
+                    const min = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.min(e.indices)])
+                    const max = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.max(e.indices)])
+                    if (min !== -1 && max !== -1) {
+                        if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true
+                    }
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[e.indices[i]])
+                        if (idx !== -1) {
+                            if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return changed
+}

+ 101 - 33
src/mol-geo/representation/structure/visual/util/polymer.ts

@@ -4,9 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, ElementIndex } from 'mol-model/structure';
-import { Segmentation, OrderedSet, Interval } from 'mol-data/int';
+import { Unit, ElementIndex, StructureElement, Link } from 'mol-model/structure';
 import SortedRanges from 'mol-data/int/sorted-ranges';
+import { LocationIterator } from '../../../../util/location-iterator';
+import { PickingId } from '../../../../util/picking';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { EmptyLoci, Loci } from 'mol-model/loci';
 
 export * from './polymer/backbone-iterator'
 export * from './polymer/gap-iterator'
@@ -29,41 +32,106 @@ export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
     }
 }
 
-export function getPolymerElementCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements)
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements)
-            while (polymerIt.hasNext) {
-                const polymerSegment = polymerIt.move()
-                residueIt.setSegment(polymerSegment)
-                while (residueIt.hasNext) {
-                    const residueSegment = residueIt.move()
-                    const { start, end } = residueSegment
-                    if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+export namespace PolymerLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const polymerElements = group.units[0].polymerElements
+        const groupCount = polymerElements.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = polymerElements[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export namespace PolymerGapLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const gapElements = group.units[0].gapElements
+        const groupCount = gapElements.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = gapElements[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export function getPolymerElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const unitIndex = OrderedSet.indexOf(unit.elements, unit.polymerElements[groupId]) as StructureElement.UnitIndex
+        if (unitIndex !== -1) {
+            const indices = OrderedSet.ofSingleton(unitIndex)
+            return StructureElement.Loci([{ unit, indices }])
+        }
+    }
+    return EmptyLoci
+}
+
+export function markPolymerElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    const groupCount = group.units[0].polymerElements.length
+
+    let changed = false
+    if (StructureElement.isLoci(loci)) {
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
+                if (Interval.is(e.indices)) {
+                    const min =  + OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.min(e.indices)])
+                    const max = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.max(e.indices)])
+                    if (min !== -1 && max !== -1) {
+                        if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true
+                    }
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[e.indices[i]])
+                        if (idx !== -1) {
+                            if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                        }
+                    }
                 }
             }
-            break
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            while (polymerIt.hasNext) {
-                const { start, end } = polymerIt.move()
-                count += OrderedSet.intersectionSize(Interval.ofBounds(elements[start], elements[end - 1]), elements)
-            }
-            break
+        }
     }
-    return count
+    return changed
 }
 
-export function getPolymerGapCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements)
-    while (gapIt.hasNext) {
-        const { start, end } = gapIt.move()
-        if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+export function getPolymerGapElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const unitIndexA = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId]) as StructureElement.UnitIndex
+        const unitIndexB = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId % 2 ? groupId - 1 : groupId + 1]) as StructureElement.UnitIndex
+        if (unitIndexA !== -1 && unitIndexB !== -1) {
+            return Link.Loci([ Link.Location(unit, unitIndexA, unit, unitIndexB) ])
+        }
     }
-    return count
+    return EmptyLoci
 }
+
+export function markPolymerGapElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (Link.isLoci(loci)) {
+        const groupCount = group.units[0].gapElements.length
+        for (const b of loci.links) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
+                const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex])
+                const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex])
+                if (idxA !== -1 && idxB !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true
+                }
+            }
+        }
+    }
+    return changed
+}

+ 10 - 3
src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts

@@ -10,6 +10,7 @@ import Iterator from 'mol-data/iterator';
 import SortedRanges from 'mol-data/int/sorted-ranges';
 import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getPolymerRanges } from '../polymer';
+import { AtomRole } from 'mol-model/structure/model/types';
 
 /** Iterates over consecutive pairs of residues/coarse elements in polymers */
 export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePair> {
@@ -43,13 +44,19 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
     private residueSegment: Segmentation.Segment<ResidueIndex>
     hasNext: boolean = false;
 
+    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
+        const { atomicHierarchy } = this.unit.model
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
+    }
+
     move() {
         if (this.state === AtomicPolymerBackboneIteratorState.nextPolymer) {
             while (this.polymerIt.hasNext) {
                 this.residueIt.setSegment(this.polymerIt.move());
                 if (this.residueIt.hasNext) {
                     this.residueSegment = this.residueIt.move()
-                    this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace')
+                    this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
                     this.state = AtomicPolymerBackboneIteratorState.nextResidue
                     break
                 }
@@ -59,7 +66,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
         if (this.state === AtomicPolymerBackboneIteratorState.nextResidue) {
             this.residueSegment = this.residueIt.move()
             this.value.centerA.element = this.value.centerB.element
-            this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace')
+            this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
             if (!this.residueIt.hasNext) {
                 if (this.unit.model.atomicHierarchy.cyclicPolymerMap.has(this.residueSegment.index)) {
                     this.state = AtomicPolymerBackboneIteratorState.cycle
@@ -71,7 +78,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
         } else if (this.state === AtomicPolymerBackboneIteratorState.cycle) {
             const { cyclicPolymerMap } = this.unit.model.atomicHierarchy
             this.value.centerA.element = this.value.centerB.element
-            this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
+            this.value.centerB.element = this.getElementIndex(cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
             // TODO need to advance to a polymer that has two or more residues (can't assume it has)
             this.state = AtomicPolymerBackboneIteratorState.nextPolymer
         }

+ 2 - 8
src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts

@@ -5,7 +5,6 @@
  */
 
 import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/structure';
-import { SortedArray } from 'mol-data/int';
 import { AtomRole } from 'mol-model/structure/model/types';
 import Iterator from 'mol-data/iterator';
 import SortedRanges from 'mol-data/int/sorted-ranges';
@@ -40,13 +39,8 @@ export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> {
     hasNext: boolean = false;
 
     private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
-        const index = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
-        // TODO handle case when it returns -1
-        const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex
-        if (elementIndex === -1) {
-            console.log('-1', residueIndex, atomRole, index)
-        }
-        return elementIndex === -1 ? 0 as ElementIndex : elementIndex
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? this.unit.model.atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
     }
 
     move() {

+ 16 - 15
src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts

@@ -89,24 +89,25 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
         this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]]
     }
 
-    private getAtomIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
-        const { cyclicPolymerMap } = this.unit.model.atomicHierarchy
+    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
+        const { atomicHierarchy } = this.unit.model
         if (residueIndex < this.residueSegmentMin) {
-            const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMin)
+            const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMin)
             if (cyclicIndex !== undefined) {
                 residueIndex = cyclicIndex - (this.residueSegmentMin - residueIndex - 1) as ResidueIndex
             } else {
                 residueIndex = this.residueSegmentMin
             }
         } else if (residueIndex > this.residueSegmentMax) {
-            const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMax)
+            const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMax)
             if (cyclicIndex !== undefined) {
                 residueIndex = cyclicIndex + (residueIndex - this.residueSegmentMax - 1) as ResidueIndex
             } else {
                 residueIndex = this.residueSegmentMax
             }
         }
-        return getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
     }
 
     private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) {
@@ -135,19 +136,19 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
         if (this.state === AtomicPolymerTraceIteratorState.nextResidue) {
             const { index: residueIndex } = residueIt.move();
-            value.center.element = this.getAtomIndex(residueIndex, 'trace')
+            value.center.element = this.getElementIndex(residueIndex, 'trace')
 
-            this.pos(this.p0, this.getAtomIndex(residueIndex - 3 as ResidueIndex, 'trace'))
-            this.pos(this.p1, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'trace'))
-            this.pos(this.p2, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'trace'))
-            this.pos(this.p3, this.getAtomIndex(residueIndex, 'trace'))
-            this.pos(this.p4, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'trace'))
-            this.pos(this.p5, this.getAtomIndex(residueIndex + 2 as ResidueIndex, 'trace'))
-            this.pos(this.p6, this.getAtomIndex(residueIndex + 3 as ResidueIndex, 'trace'))
+            this.pos(this.p0, this.getElementIndex(residueIndex - 3 as ResidueIndex, 'trace'))
+            this.pos(this.p1, this.getElementIndex(residueIndex - 2 as ResidueIndex, 'trace'))
+            this.pos(this.p2, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'trace'))
+            this.pos(this.p3, this.getElementIndex(residueIndex, 'trace'))
+            this.pos(this.p4, this.getElementIndex(residueIndex + 1 as ResidueIndex, 'trace'))
+            this.pos(this.p5, this.getElementIndex(residueIndex + 2 as ResidueIndex, 'trace'))
+            this.pos(this.p6, this.getElementIndex(residueIndex + 3 as ResidueIndex, 'trace'))
 
             // this.pos(this.v01, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'direction'))
-            this.pos(this.v12, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'direction'))
-            this.pos(this.v23, this.getAtomIndex(residueIndex, 'direction'))
+            this.pos(this.v12, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'direction'))
+            this.pos(this.v23, this.getElementIndex(residueIndex, 'direction'))
             // this.pos(this.v34, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'direction'))
 
             this.value.secStrucType = this.unit.model.properties.secondaryStructure.type[residueIndex]

+ 43 - 24
src/mol-geo/representation/util.ts

@@ -9,14 +9,12 @@ import { BaseValues } from 'mol-gl/renderable/schema';
 import { MeshValues, RenderableState } from 'mol-gl/renderable';
 import { defaults } from 'mol-util';
 import { Structure } from 'mol-model/structure';
-import { fillSerial } from 'mol-util/array';
-import { Mat4 } from 'mol-math/linear-algebra';
 
 export const DefaultBaseProps = {
     alpha: 1,
     visible: true,
     depthMask: true,
-    useFog: true,
+    useFog: false,
     quality: 'auto' as VisualQuality
 }
 export type BaseProps = typeof DefaultBaseProps
@@ -29,22 +27,20 @@ export const DefaultMeshProps = {
 }
 export type MeshProps = typeof DefaultMeshProps
 
-const identityTransform = new Float32Array(16)
-Mat4.toArray(Mat4.identity(), identityTransform, 0)
-export function createIdentityTransform(transforms?: ValueCell<Float32Array>) {
-    return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform)
+export const DefaultPointProps = {
+    ...DefaultBaseProps,
+    pointSizeAttenuation: true
 }
+export type PointProps = typeof DefaultPointProps
 
 type Counts = { drawCount: number, groupCount: number, instanceCount: number }
 
 export function createBaseValues(props: Required<BaseProps>, counts: Counts) {
     return {
         uAlpha: ValueCell.create(props.alpha),
-        uInstanceCount: ValueCell.create(counts.instanceCount),
         uGroupCount: ValueCell.create(counts.groupCount),
-        aInstance: ValueCell.create(fillSerial(new Float32Array(counts.instanceCount))),
         drawCount: ValueCell.create(counts.drawCount),
-        instanceCount: ValueCell.create(counts.instanceCount),
+        dUseFog: ValueCell.create(props.useFog),
     }
 }
 
@@ -54,7 +50,13 @@ export function createMeshValues(props: Required<MeshProps>, counts: Counts) {
         dDoubleSided: ValueCell.create(props.doubleSided),
         dFlatShaded: ValueCell.create(props.flatShaded),
         dFlipSided: ValueCell.create(props.flipSided),
-        dUseFog: ValueCell.create(props.useFog),
+    }
+}
+
+export function createPointValues(props: Required<PointProps>, counts: Counts) {
+    return {
+        ...createBaseValues(props, counts),
+        dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
     }
 }
 
@@ -67,6 +69,7 @@ export function createRenderableState(props: Required<BaseProps>): RenderableSta
 
 export function updateBaseValues(values: BaseValues, props: Required<BaseProps>) {
     ValueCell.updateIfChanged(values.uAlpha, props.alpha)
+    ValueCell.updateIfChanged(values.dUseFog, props.useFog)
 }
 
 export function updateMeshValues(values: MeshValues, props: Required<MeshProps>) {
@@ -74,7 +77,6 @@ export function updateMeshValues(values: MeshValues, props: Required<MeshProps>)
     ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided)
     ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded)
     ValueCell.updateIfChanged(values.dFlipSided, props.flipSided)
-    ValueCell.updateIfChanged(values.dUseFog, props.useFog)
 }
 
 export function updateRenderableState(state: RenderableState, props: Required<BaseProps>) {
@@ -82,20 +84,32 @@ export function updateRenderableState(state: RenderableState, props: Required<Ba
     state.depthMask = props.depthMask
 }
 
-export type VisualQuality = 'custom' | 'auto' | 'highest' | 'high' | 'medium' | 'low' | 'lowest'
+export const VisualQualityInfo = {
+    'custom': {},
+    'auto': {},
+    'highest': {},
+    'high': {},
+    'medium': {},
+    'low': {},
+    'lowest': {},
+}
+export type VisualQuality = keyof typeof VisualQualityInfo
+export const VisualQualityNames = Object.keys(VisualQualityInfo)
 
-interface QualityProps {
+export interface QualityProps {
     quality: VisualQuality
     detail: number
     radialSegments: number
+    linearSegments: number
 }
 
-export function getQualityProps(props: Partial<QualityProps>, structure: Structure) {
+export function getQualityProps(props: Partial<QualityProps>, structure?: Structure) {
     let quality = defaults(props.quality, 'auto' as VisualQuality)
-    let detail = 1
-    let radialSegments = 12
+    let detail = defaults(props.detail, 1)
+    let radialSegments = defaults(props.radialSegments, 12)
+    let linearSegments = defaults(props.linearSegments, 8)
 
-    if (quality === 'auto') {
+    if (quality === 'auto' && structure) {
         const score = structure.elementCount
         if (score > 500_000) {
             quality = 'lowest'
@@ -110,33 +124,38 @@ export function getQualityProps(props: Partial<QualityProps>, structure: Structu
 
     switch (quality) {
         case 'highest':
-            detail = 2
+            detail = 3
             radialSegments = 36
+            linearSegments = 18
             break
         case 'high':
-            detail = 1
+            detail = 2
             radialSegments = 24
+            linearSegments = 12
             break
         case 'medium':
-            detail = 0
+            detail = 1
             radialSegments = 12
+            linearSegments = 8
             break
         case 'low':
             detail = 0
             radialSegments = 5
+            linearSegments = 3
             break
         case 'lowest':
             detail = 0
             radialSegments = 3
+            linearSegments = 2
             break
         case 'custom':
-            detail = defaults(props.detail, 1)
-            radialSegments = defaults(props.radialSegments, 12)
+            // use defaults or given props as set above
             break
     }
 
     return {
         detail,
-        radialSegments
+        radialSegments,
+        linearSegments
     }
 }

+ 12 - 11
src/mol-geo/representation/volume/index.ts

@@ -27,31 +27,32 @@ export function VolumeRepresentation<P extends VolumeProps>(visualCtor: (volumeD
     let _volumeData: VolumeData
     let _props: P
 
-    function create(volumeData: VolumeData, props: Partial<P> = {}) {
+    function createOrUpdate(props: Partial<P> = {}, volumeData?: VolumeData) {
         _props = Object.assign({}, DefaultVolumeProps, _props, props)
         return Task.create('VolumeRepresentation.create', async ctx => {
-            _volumeData = volumeData
-            const visual = visualCtor(_volumeData)
-            await visual.create(ctx, _volumeData, props)
-            renderObjects.push(visual.renderObject)
+            if (volumeData) {
+                _volumeData = volumeData
+                const visual = visualCtor(_volumeData)
+                await visual.createOrUpdate(ctx, props, _volumeData)
+                if (visual.renderObject) renderObjects.push(visual.renderObject)
+            } else {
+                throw new Error('missing volumeData')
+            }
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('VolumeRepresentation.update', async ctx => {})
-    }
-
     return {
+        label: 'Volume mesh',
         get renderObjects () { return renderObjects },
         get props () { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci(pickingId: PickingId) {
             // TODO
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
             // TODO
+            return false
         },
         destroy() {
             // TODO

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

@@ -57,9 +57,11 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
 
     return {
         get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, volume: VolumeData, props: SurfaceProps = {}) {
+        async createOrUpdate(ctx: RuntimeContext, props: SurfaceProps = {}, volume?: VolumeData) {
             props = { ...DefaultSurfaceProps, ...props }
 
+            if (!volume) return
+
             const mesh = await computeVolumeSurface(volume, curProps.isoValue).runAsChild(ctx)
             if (!props.flatShaded) {
                 Mesh.computeNormalsImmediate(mesh)
@@ -97,16 +99,13 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
 
             renderObject = createMeshRenderObject(values, state)
         },
-        async update(ctx: RuntimeContext, props: SurfaceProps) {
-            // TODO
-            return false
-        },
         getLoci(pickingId: PickingId) {
             // TODO
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
             // TODO
+            return false
         },
         destroy() {
             // TODO

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

@@ -9,7 +9,9 @@ import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
 import { Color } from 'mol-util/color';
 import { Vec2, Vec3 } from 'mol-math/linear-algebra';
 import { LocationIterator } from './location-iterator';
-import { Location, NullLocation } from 'mol-model/location';
+import { NullLocation } from 'mol-model/location';
+import { LocationColor } from 'mol-view/theme/color';
+import { RuntimeContext } from 'mol-task';
 
 export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance'
 
@@ -21,8 +23,6 @@ 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 {
@@ -49,8 +49,8 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
 }
 
 /** Creates color uniform */
-export function createUniformColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
-    return createValueColor(colorFn(NullLocation, false), colorData)
+export async function createUniformColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
+    return createValueColor(color(NullLocation, false), colorData)
 }
 
 export function createTextureColor(colors: TextureImage, type: ColorType, colorData?: ColorData): ColorData {
@@ -73,36 +73,51 @@ export function createTextureColor(colors: TextureImage, type: ColorType, colorD
 }
 
 /** Creates color texture with color for each instance/unit */
-export function createInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { instanceCount} = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= instanceCount * 3 ? colorData.tColor.ref.value : createTextureImage(instanceCount, 3)
-    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+    let i = 0
+    while (locationIt.hasNext) {
         const { location, isSecondary, instanceIndex } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, instanceIndex * 3)
+        Color.toArray(color(location, isSecondary), colors.array, instanceIndex * 3)
         locationIt.skipInstance()
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating instance colors', current: i, max: instanceCount });
+        }
+        ++i
     }
     return createTextureColor(colors, 'instance', colorData)
 }
 
 /** Creates color texture with color for each group (i.e. shared across instances/units) */
-export function createGroupColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createGroupColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { groupCount } = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= groupCount * 3 ? colorData.tColor.ref.value : createTextureImage(groupCount, 3)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const { location, isSecondary, groupIndex } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, groupIndex * 3)
+        Color.toArray(color(location, isSecondary), colors.array, groupIndex * 3)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group colors', current: i, max: groupCount });
+        }
+        ++i
     }
     return createTextureColor(colors, 'group', colorData)
 }
 
 /** Creates color texture with color for each group in each instance (i.e. for each unit) */
-export function createGroupInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createGroupInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
     const colors = colorData && colorData.tColor.ref.value.array.length >= count * 3 ? colorData.tColor.ref.value : createTextureImage(count, 3)
-    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+    let i = 0
+    while (locationIt.hasNext) {
         const { location, isSecondary, index } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, index * 3)
+        Color.toArray(color(location, isSecondary), colors.array, index * 3)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group instance colors', current: i, max: count });
+        }
+        ++i
     }
     return createTextureColor(colors, 'groupInstance', colorData)
 }

+ 4 - 1
src/mol-geo/util/location-iterator.ts

@@ -28,6 +28,8 @@ export interface LocationIterator extends Iterator<LocationValue> {
     readonly isNextNewInstance: boolean
     readonly groupCount: number
     readonly instanceCount: number
+    /** If true, may have multiple units per instance; if false one unit per instance */
+    readonly isComplex: boolean
     move(): LocationValue
     reset(): void
     skipInstance(): void
@@ -36,7 +38,7 @@ export interface LocationIterator extends Iterator<LocationValue> {
 type LocationGetter = (groupIndex: number, instanceIndex: number) => Location
 type IsSecondaryGetter = (groupIndex: number, instanceIndex: number) => boolean
 
-export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
+export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isComplex = false, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
     const value: LocationValue = {
         location: NullLocation as Location,
         index: 0,
@@ -55,6 +57,7 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
         get isNextNewInstance () { return isNextNewInstance },
         get groupCount () { return groupCount },
         get instanceCount () { return instanceCount },
+        isComplex,
         move() {
             if (hasNext) {
                 value.groupIndex = groupIndex

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

@@ -18,7 +18,6 @@ export enum MarkerAction {
     RemoveHighlight,
     Select,
     Deselect,
-    ToggleSelect,
     Clear
 }
 
@@ -30,42 +29,26 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
             case MarkerAction.Highlight:
                 if (v % 2 === 0) {
                     v += 1
-                    changed = true
                 }
                 break
             case MarkerAction.RemoveHighlight:
                 if (v % 2 !== 0) {
                     v -= 1
-                    changed = true
                 }
                 break
             case MarkerAction.Select:
                 v += 2
-                changed = true
                 break
             case MarkerAction.Deselect:
                 if (v >= 2) {
                     v -= 2
-                    changed = true
                 }
                 break
-            case MarkerAction.ToggleSelect:
-                if (v === 0) {
-                    v = 2
-                } else if (v === 1) {
-                    v = 3
-                } else if (v === 2) {
-                    v = 0
-                } else {
-                    v -= 2
-                }
-                changed = true
-                break
             case MarkerAction.Clear:
                 v = 0
-                changed = true
                 break
         }
+        changed = array[i] !== v || changed
         array[i] = v
     }
     return changed

+ 20 - 4
src/mol-geo/util/size-data.ts

@@ -9,6 +9,7 @@ import { Vec2 } from 'mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
 import { LocationIterator } from './location-iterator';
 import { Location, NullLocation } from 'mol-model/location';
+import { RuntimeContext } from 'mol-task';
 
 export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance'
 
@@ -48,7 +49,7 @@ export function createValueSize(value: number, sizeData?: SizeData): SizeData {
 }
 
 /** Creates size uniform */
-export function createUniformSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createUniformSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     return createValueSize(sizeFn(NullLocation), sizeData)
 }
 
@@ -72,36 +73,51 @@ export function createTextureSize(sizes: TextureImage, type: SizeType, sizeData?
 }
 
 /** Creates size texture with size for each instance/unit */
-export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { instanceCount} = locationIt
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= instanceCount ? sizeData.tSize.ref.value : createTextureImage(instanceCount, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.instanceIndex] = sizeFn(v.location)
         locationIt.skipInstance()
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating instance sizes', current: i, max: instanceCount });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'instance', sizeData)
 }
 
 /** Creates size texture with size for each group (i.e. shared across instances/units) */
-export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createGroupSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { groupCount } = locationIt
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= groupCount ? sizeData.tSize.ref.value : createTextureImage(groupCount, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.groupIndex] = sizeFn(v.location)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group sizes', current: i, max: groupCount });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'group', sizeData)
 }
 
 /** Creates size texture with size for each group in each instance (i.e. for each unit) */
-export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createGroupInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= count ? sizeData.tSize.ref.value : createTextureImage(count, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.index] = sizeFn(v.location)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group instance sizes', current: i, max: count });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'groupInstance', sizeData)
 }

+ 52 - 0
src/mol-geo/util/transform-data.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 { ValueCell } from 'mol-util';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { fillSerial } from 'mol-util/array';
+import { Unit } from 'mol-model/structure';
+
+export type TransformData = {
+    aTransform: ValueCell<Float32Array>,
+    uInstanceCount: ValueCell<number>,
+    instanceCount: ValueCell<number>,
+    aInstance: ValueCell<Float32Array>,
+}
+
+export function _createTransforms(transformArray: Float32Array, instanceCount: number, transformData?: TransformData): TransformData {
+    if (transformData) {
+        ValueCell.update(transformData.aTransform, transformArray)
+        ValueCell.update(transformData.uInstanceCount, instanceCount)
+        ValueCell.update(transformData.instanceCount, instanceCount)
+        const aInstance = transformData.aInstance.ref.value.length >= instanceCount ? transformData.aInstance.ref.value : new Float32Array(instanceCount)
+        ValueCell.update(transformData.aInstance, fillSerial(aInstance, instanceCount))
+        return transformData
+    } else {
+        return {
+            aTransform: ValueCell.create(transformArray),
+            uInstanceCount: ValueCell.create(instanceCount),
+            instanceCount: ValueCell.create(instanceCount),
+            aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount)))
+        }
+    }
+}
+
+const identityTransform = new Float32Array(16)
+Mat4.toArray(Mat4.identity(), identityTransform, 0)
+export function createIdentityTransform(transformData?: TransformData): TransformData {
+    return _createTransforms(identityTransform, 1, transformData)
+}
+
+export function createTransforms({ units }: Unit.SymmetryGroup, transformData?: TransformData) {
+    const unitCount = units.length
+    const n = unitCount * 16
+    const array = transformData && transformData.aTransform.ref.value.length >= n ? transformData.aTransform.ref.value : new Float32Array(n)
+    for (let i = 0; i < unitCount; i++) {
+        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
+    }
+    return _createTransforms(array, unitCount, transformData)
+}
+

+ 3 - 3
src/mol-gl/renderable/util.ts

@@ -9,10 +9,10 @@ import { Mat4, Vec3 } from 'mol-math/linear-algebra'
 import { ValueCell } from 'mol-util';
 
 export function calculateTextureInfo (n: number, itemSize: number) {
-    const sqN = Math.sqrt(n * itemSize)
+    const sqN = Math.sqrt(n)
     let width = Math.ceil(sqN)
     width = width + (itemSize - (width % itemSize)) % itemSize
-    const height = width > 0 ? Math.ceil(n * itemSize / width) : 0
+    const height = width > 0 ? Math.ceil(n / width) : 0
     return { width, height, length: width * height * itemSize }
 }
 
@@ -43,7 +43,7 @@ function getPositionDataFromValues(values: PositionValues) {
     }
 }
 
-export function calculateBoundingSphereFromValues(values: PositionValues){
+export function calculateBoundingSphereFromValues(values: PositionValues) {
     const { position, positionCount, transform, transformCount } = getPositionDataFromValues(values)
     return calculateBoundingSphere(position, positionCount, transform, transformCount)
 }

+ 18 - 6
src/mol-gl/renderer.ts

@@ -92,16 +92,28 @@ namespace Renderer {
                     program.setUniforms(globalUniforms)
                     currentProgramId = program.id
                 }
-                if (r.values.dDoubleSided.ref.value) {
-                    gl.disable(gl.CULL_FACE)
+
+                if (r.values.dDoubleSided) {
+                    if (r.values.dDoubleSided.ref.value) {
+                        gl.disable(gl.CULL_FACE)
+                    } else {
+                        gl.enable(gl.CULL_FACE)
+                    }
                 } else {
-                    gl.enable(gl.CULL_FACE)
+                    // webgl default
+                    gl.disable(gl.CULL_FACE)
                 }
 
-                if (r.values.dFlipSided.ref.value) {
-                    gl.frontFace(gl.CW)
-                    gl.cullFace(gl.FRONT)
+                if (r.values.dFlipSided) {
+                    if (r.values.dFlipSided.ref.value) {
+                        gl.frontFace(gl.CW)
+                        gl.cullFace(gl.FRONT)
+                    } else {
+                        gl.frontFace(gl.CCW)
+                        gl.cullFace(gl.BACK)
+                    }
                 } else {
+                    // webgl default
                     gl.frontFace(gl.CCW)
                     gl.cullFace(gl.BACK)
                 }

+ 2 - 2
src/mol-gl/scene.ts

@@ -60,10 +60,10 @@ namespace Scene {
 
             update: () => {
                 update()
-                renderableMap.forEach((o, r) => o.update())
+                renderableMap.forEach(o => o.update())
                 boundingSphere = undefined
             },
-            
+
             add: (o: RenderObject) => {
                 if (!renderableMap.has(o)) {
                     renderableMap.set(o, createRenderable(ctx, o))

+ 17 - 8
src/mol-gl/shader-code.ts

@@ -6,25 +6,33 @@
  */
 
 import { ValueCell } from 'mol-util';
+import { idFactory } from 'mol-util/id-factory';
 
 export type DefineKind = 'boolean' | 'string'
 export type DefineType = boolean | string
 export type DefineValues = { [k: string]: ValueCell<DefineType> }
 
+const shaderCodeId = idFactory()
+
 export interface ShaderCode {
+    id: number
     vert: string
     frag: string
 }
 
-export const PointShaderCode: ShaderCode = {
-    vert: require('mol-gl/shader/point.vert'),
-    frag: require('mol-gl/shader/point.frag')
+export function ShaderCode(vert: string, frag: string): ShaderCode {
+    return { id: shaderCodeId(), vert, frag }
 }
 
-export const MeshShaderCode: ShaderCode = {
-    vert: require('mol-gl/shader/mesh.vert'),
-    frag: require('mol-gl/shader/mesh.frag')
-}
+export const PointShaderCode = ShaderCode(
+    require('mol-gl/shader/point.vert'),
+    require('mol-gl/shader/point.frag')
+)
+
+export const MeshShaderCode = ShaderCode(
+    require('mol-gl/shader/mesh.vert'),
+    require('mol-gl/shader/mesh.frag')
+)
 
 export type ShaderDefines = {
     [k: string]: ValueCell<DefineType>
@@ -47,9 +55,10 @@ function getDefinesCode (defines: ShaderDefines) {
     return lines.join('\n') + '\n'
 }
 
-export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode) {
+export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode): ShaderCode {
     const header = getDefinesCode(defines)
     return {
+        id: shaderCodeId(),
         vert: `${header}${shaders.vert}`,
         frag: `${header}${shaders.frag}`
     }

+ 1 - 1
src/mol-gl/shader/chunks/assign-color-varying.glsl

@@ -5,7 +5,7 @@
 #elif defined(dColorType_group)
     vColor.rgb = readFromTexture(tColor, aGroup, uColorTexDim).rgb;
 #elif defined(dColorType_groupInstance)
-    vColor.rgb = readFromTexture(tColor, aGroup * float(uGroupCount) + aGroup, uColorTexDim).rgb;
+    vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + aGroup, uColorTexDim).rgb;
 #elif defined(dColorType_objectPicking)
     vColor = encodeIdRGBA(float(uObjectId));
 #elif defined(dColorType_instancePicking)

+ 1 - 1
src/mol-gl/shader/chunks/assign-material-color.glsl

@@ -1,5 +1,5 @@
 #if defined(dColorType_uniform)
-    vec4 material = vec4(uColor, 1.0);
+    vec4 material = vec4(uColor, uAlpha);
 #elif defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance) || defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
     vec4 material = vColor;
 #endif

+ 5 - 1
src/mol-gl/shader/chunks/common-frag-params.glsl

@@ -6,6 +6,10 @@ uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
 varying float vMarker;
 
+varying vec3 vViewPosition;
+
 uniform float uFogNear;
 uniform float uFogFar;
-uniform vec3 uFogColor;
+uniform vec3 uFogColor;
+
+uniform float uAlpha;

+ 3 - 0
src/mol-gl/shader/chunks/common-vert-params.glsl

@@ -7,4 +7,7 @@ uniform int uGroupCount;
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
 varying float vMarker;
+
+varying vec3 vViewPosition;
+
 #pragma glslify: readFromTexture = require(../utils/read-from-texture.glsl)

+ 1 - 3
src/mol-gl/shader/mesh.frag

@@ -18,12 +18,10 @@ precision highp int;
 uniform vec3 uLightColor;
 uniform vec3 uLightAmbient;
 uniform mat4 uView;
-uniform float uAlpha;
 
 #ifndef dFlatShaded
     varying vec3 vNormal;
 #endif
-varying vec3 vViewPosition;
 
 #pragma glslify: attenuation = require(./utils/attenuation.glsl)
 #pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
@@ -74,7 +72,7 @@ void main() {
         // gl_FragColor.a = 1.0;
         // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
         gl_FragColor.rgb = finalColor;
-        gl_FragColor.a = uAlpha;
+        gl_FragColor.a = material.a;
 
         #pragma glslify: import('./chunks/apply-marker-color.glsl')
         #pragma glslify: import('./chunks/apply-fog.glsl')

+ 0 - 2
src/mol-gl/shader/mesh.vert

@@ -20,8 +20,6 @@ attribute float aGroup;
     varying vec3 vNormal;
 #endif
 
-varying vec3 vViewPosition;
-
 #pragma glslify: inverse = require(./utils/inverse.glsl)
 #pragma glslify: transpose = require(./utils/transpose.glsl)
 

+ 2 - 4
src/mol-gl/shader/point.frag

@@ -10,12 +10,10 @@ precision highp int;
 #pragma glslify: import('./chunks/common-frag-params.glsl')
 #pragma glslify: import('./chunks/color-frag-params.glsl')
 
-uniform float uAlpha;
-
 void main(){
     #pragma glslify: import('./chunks/assign-material-color.glsl')
-    
-    gl_FragColor = vec4(material, uAlpha);
+
+    gl_FragColor = material;
 
     #pragma glslify: import('./chunks/apply-marker-color.glsl')
     #pragma glslify: import('./chunks/apply-fog.glsl')

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

@@ -17,6 +17,10 @@ uniform float uViewportHeight;
     uniform float uSize;
 #elif defined(dSizeType_attribute)
     attribute float aSize;
+#elif defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance)
+    varying vec4 vSize;
+    uniform vec2 uSizeTexDim;
+    uniform sampler2D tSize;
 #endif
 
 attribute vec3 aPosition;

+ 13 - 3
src/mol-gl/webgl/program.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ShaderCode } from '../shader-code'
+import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code'
 import { Context } from './context';
 import { getUniformUpdaters, getTextureUniformUpdaters, UniformValues } from './uniform';
 import { AttributeBuffers } from './buffer';
@@ -12,6 +12,7 @@ import { TextureId, Textures } from './texture';
 import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache';
 import { idFactory } from 'mol-util/id-factory';
 import { RenderableSchema } from '../renderable/schema';
+import { hashFnv32a, hashString } from 'mol-data/util';
 
 const getNextProgramId = idFactory()
 
@@ -46,19 +47,21 @@ function getAttributeLocations(ctx: Context, program: WebGLProgram, schema: Rend
 }
 
 export interface ProgramProps {
+    defineValues: DefineValues,
     shaderCode: ShaderCode,
     schema: RenderableSchema
 }
 
 export function createProgram(ctx: Context, props: ProgramProps): Program {
     const { gl, shaderCache } = ctx
-    const { shaderCode, schema } = props
+    const { defineValues, shaderCode: _shaderCode, schema } = props
 
     const program = gl.createProgram()
     if (program === null) {
         throw new Error('Could not create WebGL program')
     }
 
+    const shaderCode = addShaderDefines(defineValues, _shaderCode)
     const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert })
     const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag })
 
@@ -113,7 +116,14 @@ export type ProgramCache = ReferenceCache<Program, ProgramProps, Context>
 
 export function createProgramCache(): ProgramCache {
     return createReferenceCache(
-        (props: ProgramProps) => JSON.stringify(props),
+        (props: ProgramProps) => {
+            const array = [ props.shaderCode.id ]
+            Object.keys(props.defineValues).forEach(k => {
+                const v = props.defineValues[k].ref.value
+                array.push(hashString(k), typeof v === 'boolean' ? v ? 1 : 0 : hashString(v))
+            })
+            return hashFnv32a(array).toString()
+        },
         (ctx: Context, props: ProgramProps) => createProgram(ctx, props),
         (program: Program) => { program.destroy() }
     )

+ 6 - 4
src/mol-gl/webgl/render-item.ts

@@ -7,7 +7,7 @@
 import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, ArrayKind } from './buffer';
 import { createTextures } from './texture';
 import { Context } from './context';
-import { ShaderCode, addShaderDefines } from '../shader-code';
+import { ShaderCode } from '../shader-code';
 import { Program } from './program';
 import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
@@ -74,7 +74,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
     Object.keys(RenderVariantDefines).forEach(k => {
         const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
         programs[k] = programCache.get(ctx, {
-            shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode),
+            defineValues: { ...defineValues, ...variantDefineValues },
+            shaderCode,
             schema
         })
     })
@@ -117,7 +118,7 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
             program.setUniforms(uniformValues)
             if (oesVertexArrayObject && vertexArray) {
                 oesVertexArrayObject.bindVertexArrayOES(vertexArray)
-                // TODO need to bind elements buffer explicitely since it is not always recorded in the VAO
+                // need to bind elements buffer explicitely since it is not always recorded in the VAO
                 if (elementsBuffer) elementsBuffer.bind()
             } else {
                 if (elementsBuffer) elementsBuffer.bind()
@@ -147,7 +148,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
                     const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
                     programs[k].free()
                     programs[k] = programCache.get(ctx, {
-                        shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode),
+                        defineValues: { ...defineValues, ...variantDefineValues },
+                        shaderCode,
                         schema
                     })
                 })

+ 1 - 1
src/mol-math/geometry/lookup3d/grid.ts

@@ -159,7 +159,7 @@ function _build(state: BuildState): Grid3D {
 function build(data: PositionData) {
     const boundingBox = Box3D.computeBounding(data);
     // need to expand the grid bounds to avoid rounding errors
-    const expandedBox = Box3D.expand(boundingBox, Vec3.create(0.5, 0.5, 0.5));
+    const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
     const boundingSphere = Sphere3D.computeBounding(data);
     const { indices } = data;
 

+ 44 - 11
src/mol-math/geometry/primitives/box3d.ts

@@ -2,9 +2,10 @@
  * Copyright (c) 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 { Vec3 } from '../../linear-algebra'
+import { Vec3, Mat4 } from '../../linear-algebra'
 import { PositionData } from '../common'
 import { OrderedSet } from 'mol-data/int';
 
@@ -12,10 +13,11 @@ interface Box3D { min: Vec3, max: Vec3 }
 
 namespace Box3D {
     export function create(min: Vec3, max: Vec3): Box3D { return { min, max }; }
+    export function empty(): Box3D { return { min: Vec3.zero(), max: Vec3.zero() }; }
 
     export function computeBounding(data: PositionData): Box3D {
-        const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
-        const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];
+        const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
+        const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
 
         const { x, y, z, indices } = data;
         for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
@@ -27,18 +29,49 @@ namespace Box3D {
             max[1] = Math.max(y[i], max[1]);
             max[2] = Math.max(z[i], max[2]);
         }
-        return { min: Vec3.create(min[0], min[1], min[2]), max: Vec3.create(max[0], max[1], max[2]) }
+        return { min, max }
     }
 
-    export function size(box: Box3D) {
-        return Vec3.sub(Vec3.zero(), box.max, box.min);
+    /** Get size of the box */
+    export function size(size: Vec3, box: Box3D): Vec3 {
+        return Vec3.sub(size, box.max, box.min);
     }
 
-    export function expand(box: Box3D, delta: Vec3): Box3D {
-        return {
-            min: Vec3.sub(Vec3.zero(), box.min, delta),
-            max: Vec3.add(Vec3.zero(), box.max, delta)
-        }
+    export function setEmpty(box: Box3D): Box3D {
+        Vec3.set(box.min, Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+        Vec3.set(box.max, -Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+        return box
+    }
+
+    /** Add point to box */
+    export function add(box: Box3D, point: Vec3): Box3D {
+        Vec3.min(box.min, box.min, point)
+        Vec3.max(box.max, box.max, point)
+        return box
+    }
+
+    /** Expand box by delta */
+    export function expand(out: Box3D, box: Box3D, delta: Vec3): Box3D {
+        Vec3.sub(out.min, box.min, delta)
+        Vec3.add(out.max, box.max, delta)
+        return out
+    }
+
+    const tmpTransformV = Vec3.zero()
+    /** Transform box with a Mat4 */
+    export function transform(out: Box3D, box: Box3D, m: Mat4): Box3D {
+        const [ minX, minY, minZ ] = box.min
+        const [ maxX, maxY, maxZ ] = box.max
+        setEmpty(out)
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, maxZ), m))
+        return out
     }
 }
 

+ 11 - 4
src/mol-math/geometry/primitives/sphere3d.ts

@@ -2,18 +2,18 @@
  * Copyright (c) 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 { Vec3 } from '../../linear-algebra'
+import { Vec3, Mat4 } from '../../linear-algebra'
 import { PositionData } from '../common'
 import { OrderedSet } from 'mol-data/int';
 
 interface Sphere3D { center: Vec3, radius: number }
 
 namespace Sphere3D {
-    export function create(center: Vec3, radius: number): Sphere3D {
-        return { center, radius };
-    }
+    export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; }
+    export function zero(): Sphere3D { return { center: Vec3.zero(), radius: 0 }; }
 
     export function computeBounding(data: PositionData): Sphere3D {
         const { x, y, z, indices } = data;
@@ -43,6 +43,13 @@ namespace Sphere3D {
 
         return { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) };
     }
+
+    /** Transform sphere with a Mat4 */
+    export function transform(out: Sphere3D, sphere: Sphere3D, m: Mat4): Sphere3D {
+        Vec3.transformMat4(out.center, sphere.center, m)
+        out.radius = sphere.radius * Mat4.getMaxScaleOnAxis(m)
+        return out
+    }
 }
 
 export { Sphere3D }

+ 3 - 3
src/mol-math/geometry/symmetry-operator.ts

@@ -92,7 +92,7 @@ function isW1(m: Mat4) {
     return m[3] === 0 && m[7] === 0 && m[11] === 0 && m[15] === 1;
 }
 
-function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[0], yy = m[4], zz = m[8], tx = m[12];
 
     if (isW1(m)) {
@@ -106,7 +106,7 @@ function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme
     }
 }
 
-function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[1], yy = m[5], zz = m[9], ty = m[13];
 
     if (isW1(m)) {
@@ -120,7 +120,7 @@ function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme
     }
 }
 
-function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[2], yy = m[6], zz = m[10], tz = m[14];
 
     if (isW1(m)) {

+ 7 - 0
src/mol-math/linear-algebra/3d/mat4.ts

@@ -873,6 +873,13 @@ namespace Mat4 {
         return out;
     }
 
+    export function getMaxScaleOnAxis(m: Mat4) {
+        const scaleXSq = m[0] * m[0] + m[1] * m[1] + m[2] * m[2]
+        const scaleYSq = m[4] * m[4] + m[5] * m[5] + m[6] * m[6]
+        const scaleZSq = m[8] * m[8] + m[9] * m[9] + m[10] * m[10]
+        return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq))
+    }
+
     /** 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 180deg around x-axis */

+ 26 - 6
src/mol-model-props/rcsb/symmetry.ts

@@ -22,8 +22,8 @@ const { str, int, float, Aliased, Vector, List } = Column.Schema;
 
 function getInstance(name: keyof AssemblySymmetry.Schema): (ctx: CifExportContext) => CifWriter.Category.Instance<any, any> {
     return function(ctx: CifExportContext) {
-        const db = AssemblySymmetry.get(ctx.model);
-        return db ? Category.ofTable(db[name]) : CifWriter.Category.Empty;
+        const assemblySymmetry = AssemblySymmetry.get(ctx.model);
+        return assemblySymmetry ? Category.ofTable(assemblySymmetry.db[name]) : CifWriter.Category.Empty;
     }
 }
 
@@ -38,7 +38,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb
     const clusterRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_cluster>[] = []
     const axisRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_axis>[] = []
 
-    let id = 0
+    let id = 1 // start feature ids at 1
     for (let i = 0, il = assemblies.length; i < il; ++i) {
         const { assembly_id: _assembly_id, rcsb_assembly_symmetry } = assemblies[i]
         if (!rcsb_assembly_symmetry) continue
@@ -65,7 +65,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb
                     clusterRows.push({
                         feature_id: id,
                         avg_rmsd: c.avg_rmsd || 0, // TODO upstream, should not be nullable, or???
-                        members: c.members as string[]
+                        members: c.members as string[] // TODO upstream, array members should not be nullable
                     })
                 }
             }
@@ -109,6 +109,26 @@ const _Descriptor: ModelPropertyDescriptor = {
 
 const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql')
 
+export interface AssemblySymmetry {
+    db: AssemblySymmetry.Database
+    getFeatures(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_feature']>
+    getClusters(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_cluster']>
+    getAxes(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_axis']>
+}
+
+export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetry {
+    const f = db.rcsb_assembly_symmetry_feature
+    const c = db.rcsb_assembly_symmetry_cluster
+    const a = db.rcsb_assembly_symmetry_axis
+
+    return {
+        db,
+        getFeatures: (assemblyId: string) => Table.pick(f, f._schema, i => f.assembly_id.value(i) === assemblyId),
+        getClusters: (featureId: number) => Table.pick(c, c._schema, i => c.feature_id.value(i) === featureId),
+        getAxes: (featureId: number) => Table.pick(a, a._schema, i => a.feature_id.value(i) === featureId)
+    }
+}
+
 export namespace AssemblySymmetry {
     export const Schema = {
         rcsb_assembly_symmetry_feature: {
@@ -195,11 +215,11 @@ export namespace AssemblySymmetry {
         }
 
         model.customProperties.add(Descriptor);
-        model._staticPropertyData.__AssemblySymmetry__ = db;
+        model._staticPropertyData.__AssemblySymmetry__ = AssemblySymmetry(db);
         return true;
     }
 
-    export function get(model: Model): Database | undefined {
+    export function get(model: Model): AssemblySymmetry | undefined {
         return model._staticPropertyData.__AssemblySymmetry__;
     }
 }

+ 15 - 0
src/mol-model/loci.ts

@@ -22,4 +22,19 @@ export function isEmptyLoci(x: any): x is EmptyLoci {
     return !!x && x.kind === 'empty-loci';
 }
 
+export function areLociEqual(lociA: Loci, lociB: Loci) {
+    if (isEveryLoci(lociA) && isEveryLoci(lociB)) return true
+    if (isEmptyLoci(lociA) && isEmptyLoci(lociB)) return true
+    if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) {
+        return StructureElement.areLociEqual(lociA, lociB)
+    }
+    if (Link.isLoci(lociA) && Link.isLoci(lociB)) {
+        return Link.areLociEqual(lociA, lociB)
+    }
+    if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) {
+        return Shape.areLociEqual(lociA, lociB)
+    }
+    return false
+}
+
 export type Loci =  StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci

+ 11 - 0
src/mol-model/shape/shape.ts

@@ -68,4 +68,15 @@ export namespace Shape {
     export function isLoci(x: any): x is Loci {
         return !!x && x.kind === 'group-loci';
     }
+
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.groups.length !== b.groups.length) return false
+        for (let i = 0, il = a.groups.length; i < il; ++i) {
+            const groupA = a.groups[i]
+            const groupB = b.groups[i]
+            if (groupA.shape.id !== groupB.shape.id) return false
+            if (!OrderedSet.areEqual(groupA.ids, groupB.ids)) return false
+        }
+        return true
+    }
 }

+ 6 - 1
src/mol-model/structure/model/formats/mmcif.ts

@@ -141,7 +141,12 @@ function getFormatData(format: mmCIF_Format): FormatData {
 function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, formatData: FormatData, previous?: Model): Model {
     const atomic = getAtomicHierarchyAndConformation(format, atom_site, entities, formatData, previous);
     if (previous && atomic.sameAsPrevious) {
-        return { ...previous, atomicConformation: atomic.conformation };
+        return {
+            ...previous,
+            id: UUID.create(),
+            modelNum: atom_site.pdbx_PDB_model_num.value(0),
+            atomicConformation: atomic.conformation
+        };
     }
 
     const coarse = EmptyIHMCoarse;

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

@@ -110,14 +110,14 @@ function expandOperators1(operatorNames: string[][], list: string[][], i: number
 function getAssemblyOperators(matrices: Matrices, operatorNames: string[][], startIndex: number) {
     const operators: SymmetryOperator[] = [];
 
-    let index = startIndex;
     for (let op of operatorNames) {
         let m = Mat4.identity();
         for (let i = 0; i < op.length; i++) {
             Mat4.mul(m, m, matrices.get(op[i])!);
         }
-        index++;
-        operators[operators.length] = SymmetryOperator.create(`A-${index}`, m);
+        // TODO currently using the original operator name for the symmetry operator to be able
+        // to link it to the original operator but it might be clearer to introduce an extra field???
+        operators[operators.length] = SymmetryOperator.create(`A-${op.join(',')}`, m);
     }
 
     return operators;

+ 13 - 0
src/mol-model/structure/structure/carbohydrates/constants.ts

@@ -174,6 +174,19 @@ const Monosaccharides: SaccharideComponent[] = [
     { abbr: 'Psi', name: 'Psicose', color: SaccharideColors.Pink, type: SaccharideType.Assigned },
 ]
 
+export const MonosaccharidesColorTable: [string, Color][] = [
+    ['Glc-family', SaccharideColors.Blue],
+    ['Man-family', SaccharideColors.Green],
+    ['Gal-family', SaccharideColors.Yellow],
+    ['Gul-family', SaccharideColors.Orange],
+    ['Alt-family', SaccharideColors.Pink],
+    ['All-family', SaccharideColors.Purple],
+    ['Tal-family', SaccharideColors.LightBlue],
+    ['Ido-family', SaccharideColors.Blue],
+    ['Fuc-family', SaccharideColors.Red],
+    ['Generic/Unknown/Secondary', SaccharideColors.Secondary],
+]
+
 const CommonSaccharideNames: { [k: string]: string[] } = {
     // Hexose
     Glc: [

+ 11 - 0
src/mol-model/structure/structure/element.ts

@@ -63,6 +63,17 @@ namespace StructureElement {
         return !!x && x.kind === 'element-loci';
     }
 
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.elements.length !== b.elements.length) return false
+        for (let i = 0, il = a.elements.length; i < il; ++i) {
+            const elementA = a.elements[i]
+            const elementB = b.elements[i]
+            if (elementA.unit.id !== elementB.unit.id) return false
+            if (!OrderedSet.areEqual(elementA.indices, elementB.indices)) return false
+        }
+        return true
+    }
+
     export function isLocation(x: any): x is StructureElement {
         return !!x && x.kind === 'element-location';
     }

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

@@ -8,7 +8,7 @@ import { IntMap, SortedArray, Iterator, Segmentation } from 'mol-data/int'
 import { UniqueArray } from 'mol-data/generic'
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model, ElementIndex } from '../model'
-import { sort, arraySwap, hash1, sortArray } from 'mol-data/util';
+import { sort, arraySwap, hash1, sortArray, hashString } from 'mol-data/util';
 import StructureElement from './element'
 import Unit from './unit'
 import { StructureLookup3D } from './util/lookup3d';
@@ -22,9 +22,12 @@ import { ResidueIndex } from '../model/indexing';
 import { Carbohydrates } from './carbohydrates/data';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { idFactory } from 'mol-util/id-factory';
 
 class Structure {
+    /** Maps unit.id to unit */
     readonly unitMap: IntMap<Unit>;
+    /** Array of all units in the structure, sorted by unit.id */
     readonly units: ReadonlyArray<Unit>;
 
     private _props: {
@@ -66,6 +69,7 @@ class Structure {
         return hash;
     }
 
+    /** Returns a new element location iterator */
     elementLocations(): Iterator<StructureElement> {
         return new Structure.ElementLocationIterator(this);
     }
@@ -197,9 +201,10 @@ namespace Structure {
 
     export class StructureBuilder {
         private units: Unit[] = [];
+        private invariantId = idFactory()
 
         addUnit(kind: Unit.Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
-            const unit = Unit.create(this.units.length, kind, model, operator, elements);
+            const unit = Unit.create(this.units.length, this.invariantId(), kind, model, operator, elements);
             this.units.push(unit);
             return unit;
         }
@@ -225,6 +230,10 @@ namespace Structure {
         return s.hashCode;
     }
 
+    export function conformationHash(s: Structure) {
+        return hashString(s.units.map(u => Unit.conformationId(u)).join('|'))
+    }
+
     export function areEqual(a: Structure, b: Structure) {
         if (a.elementCount !== b.elementCount) return false;
         const len = a.units.length;

+ 5 - 13
src/mol-model/structure/structure/symmetry.ts

@@ -1,7 +1,8 @@
 /**
- * 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 Structure from './structure'
@@ -10,7 +11,7 @@ import { ModelSymmetry } from '../model'
 import { Task, RuntimeContext } from 'mol-task';
 import { SortedArray } from 'mol-data/int';
 import Unit from './unit';
-import { EquivalenceClasses, hash2 } from 'mol-data/util';
+import { EquivalenceClasses } from 'mol-data/util';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry';
 
@@ -58,16 +59,12 @@ namespace StructureSymmetry {
         return Task.create('Build NCS', ctx => _buildNCS(ctx, structure));
     }
 
-    function hashUnit(u: Unit) {
-        return hash2(u.invariantId, SortedArray.hashCode(u.elements));
-    }
-
     export function areUnitsEquivalent(a: Unit, b: Unit) {
         return a.invariantId === b.invariantId && a.model.id === b.model.id && SortedArray.areEqual(a.elements, b.elements);
     }
 
     export function UnitEquivalenceBuilder() {
-        return EquivalenceClasses<number, Unit>(hashUnit, areUnitsEquivalent);
+        return EquivalenceClasses<number, Unit>(Unit.hashUnit, areUnitsEquivalent);
     }
 
     export function computeTransformGroups(s: Structure): ReadonlyArray<Unit.SymmetryGroup> {
@@ -76,12 +73,7 @@ namespace StructureSymmetry {
 
         const ret: Unit.SymmetryGroup[] = [];
         for (const eqUnits of groups.groups) {
-            const first = s.unitMap.get(eqUnits[0]);
-            ret.push({
-                elements: first.elements,
-                units: eqUnits.map(id => s.unitMap.get(id)),
-                hashCode: hashUnit(first)
-            });
+            ret.push(Unit.SymmetryGroup(eqUnits.map(id => s.unitMap.get(id))))
         }
 
         return ret;

+ 112 - 25
src/mol-model/structure/structure/unit.ts

@@ -2,18 +2,22 @@
  * 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 { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model } from '../model'
 import { GridLookup3D, Lookup3D } from 'mol-math/geometry'
-import { idFactory } from 'mol-util/id-factory';
 import { IntraUnitLinks, computeIntraUnitBonds } from './unit/links'
 import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation } from '../model/properties/coarse';
 import { ValueRef } from 'mol-util';
 import { UnitRings } from './unit/rings';
 import StructureElement from './element'
-import { ChainIndex, ResidueIndex } from '../model/indexing';
+import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing';
+import { IntMap, SortedArray } from 'mol-data/int';
+import { hash2 } from 'mol-data/util';
+import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer';
+import { getNucleotideElements } from './util/nucleotide';
 
 // A building block of a structure that corresponds to an atomic or a coarse grained representation
 // 'conveniently grouped together'.
@@ -27,27 +31,54 @@ namespace Unit {
     export function isSpheres(u: Unit): u is Spheres { return u.kind === Kind.Spheres; }
     export function isGaussians(u: Unit): u is Gaussians { return u.kind === Kind.Gaussians; }
 
-    export function create(id: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
+    export function create(id: number, invariantId: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
         switch (kind) {
-            case Kind.Atomic: return new Atomic(id, unitIdFactory(), model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties());
-            case Kind.Spheres: return createCoarse(id, unitIdFactory(), model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)));
-            case Kind.Gaussians: return createCoarse(id, unitIdFactory(), model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)));
+            case Kind.Atomic: return new Atomic(id, invariantId, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties());
+            case Kind.Spheres: return createCoarse(id, invariantId, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)), CoarseProperties());
+            case Kind.Gaussians: return createCoarse(id, invariantId, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)), CoarseProperties());
         }
     }
 
     /** A group of units that differ only by symmetry operators. */
     export type SymmetryGroup = {
-        readonly elements: StructureElement.Set,
+        readonly elements: StructureElement.Set
         readonly units: ReadonlyArray<Unit>
+        /** Maps unit.id to index of unit in units array */
+        readonly unitIndexMap: IntMap<number>
         readonly hashCode: number
     }
 
-    /** Find index of unit with given id, returns -1 if not found */
-    export function findUnitById(id: number, units: ReadonlyArray<Unit>) {
-        for (let i = 0, il = units.length; i < il; ++i) {
-            if (units[i].id === id) return i
+    function getUnitIndexMap(units: Unit[]) {
+        const unitIndexMap = IntMap.Mutable<number>();
+        for (let i = 0, _i = units.length; i < _i; i++) {
+            unitIndexMap.set(units[i].id, i);
+        }
+        return unitIndexMap
+    }
+
+    export function SymmetryGroup(units: Unit[]) {
+        const props: {
+            unitIndexMap?: IntMap<number>
+        } = {}
+
+        return {
+            elements: units[0].elements,
+            units,
+            get unitIndexMap () {
+                if (props.unitIndexMap) return props.unitIndexMap
+                props.unitIndexMap = getUnitIndexMap(units)
+                return props.unitIndexMap
+            },
+            hashCode: hashUnit(units[0])
         }
-        return -1
+    }
+
+    export function conformationId (unit: Unit) {
+        return Unit.isAtomic(unit) ? unit.model.atomicConformation.id : unit.model.coarseConformation.id
+    }
+
+    export function hashUnit(u: Unit) {
+        return hash2(u.invariantId, SortedArray.hashCode(u.elements));
     }
 
     export interface Base {
@@ -62,6 +93,8 @@ namespace Unit {
         applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
 
         readonly lookup3d: Lookup3D
+        readonly polymerElements: SortedArray<ElementIndex>
+        readonly gapElements: SortedArray<ElementIndex>
     }
 
     function getSphereRadiusFunc(model: Model) {
@@ -74,8 +107,6 @@ namespace Unit {
         return (i: number) => 0;
     }
 
-    const unitIdFactory = idFactory();
-
     // A bulding block of a structure that corresponds
     // to a "natural group of atoms" (most often a "chain")
     // together with a tranformation (rotation and translation)
@@ -127,6 +158,24 @@ namespace Unit {
             return this.props.rings.ref;
         }
 
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getAtomicPolymerElements(this);
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getAtomicGapElements(this);
+            return this.props.gapElements.ref;
+        }
+
+        get nucleotideElements() {
+            if (this.props.nucleotideElements.ref) return this.props.nucleotideElements.ref;
+            this.props.nucleotideElements.ref = getNucleotideElements(this);
+            return this.props.nucleotideElements.ref;
+        }
+
         getResidueIndex(elementIndex: StructureElement.UnitIndex) {
             return this.model.atomicHierarchy.residueAtomSegments.index[this.elements[elementIndex]];
         }
@@ -148,10 +197,20 @@ namespace Unit {
         lookup3d: ValueRef<Lookup3D | undefined>,
         links: ValueRef<IntraUnitLinks | undefined>,
         rings: ValueRef<UnitRings | undefined>
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        nucleotideElements: ValueRef<SortedArray<ElementIndex> | undefined>
     }
 
     function AtomicProperties(): AtomicProperties {
-        return { lookup3d: ValueRef.create(void 0), links: ValueRef.create(void 0), rings: ValueRef.create(void 0) };
+        return {
+            lookup3d: ValueRef.create(void 0),
+            links: ValueRef.create(void 0),
+            rings: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+            nucleotideElements: ValueRef.create(void 0),
+        };
     }
 
     class Coarse<K extends Kind.Gaussians | Kind.Spheres, C extends CoarseSphereConformation | CoarseGaussianConformation> implements Base {
@@ -166,32 +225,45 @@ namespace Unit {
         readonly coarseElements: CoarseElements;
         readonly coarseConformation: C;
 
+        private props: CoarseProperties;
+
         getChild(elements: StructureElement.Set): Unit {
             if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */;
-            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation);
+            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation, CoarseProperties());
         }
 
         applyOperator(id: number, operator: SymmetryOperator, dontCompose = false): Unit {
             const op = dontCompose ? operator : SymmetryOperator.compose(this.conformation.operator, operator);
-            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r));
-            (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
+            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r), this.props);
+            // (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
             return ret;
         }
 
-        private _lookup3d: ValueRef<Lookup3D | undefined> = ValueRef.create(void 0);
         get lookup3d() {
-            if (this._lookup3d.ref) return this._lookup3d.ref;
+            if (this.props.lookup3d.ref) return this.props.lookup3d.ref;
             // TODO: support sphere radius?
             const { x, y, z } = this.getCoarseElements();
-            this._lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
-            return this._lookup3d.ref;
+            this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
+            return this.props.lookup3d.ref;
+        }
+
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getCoarsePolymerElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getCoarseGapElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.gapElements.ref;
         }
 
         private getCoarseElements() {
             return this.kind === Kind.Spheres ? this.model.coarseConformation.spheres : this.model.coarseConformation.gaussians;
         }
 
-        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping) {
+        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) {
             this.kind = kind;
             this.id = id;
             this.invariantId = invariantId;
@@ -200,11 +272,26 @@ namespace Unit {
             this.conformation = conformation;
             this.coarseElements = kind === Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
             this.coarseConformation = (kind === Kind.Spheres ? model.coarseConformation.spheres : model.coarseConformation.gaussians) as C;
+            this.props = props;
         }
     }
 
-    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping): Unit {
-        return new Coarse(id, invariantId, model, kind, elements, conformation) as any as Unit /** lets call this an ugly temporary hack */;
+    interface CoarseProperties {
+        lookup3d: ValueRef<Lookup3D | undefined>,
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
+    }
+
+    function CoarseProperties(): CoarseProperties {
+        return {
+            lookup3d: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+        };
+    }
+
+    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties): Unit {
+        return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */;
     }
 
     export class Spheres extends Coarse<Kind.Spheres, CoarseSphereConformation> { }

+ 15 - 0
src/mol-model/structure/structure/unit/links.ts

@@ -32,6 +32,13 @@ namespace Link {
         return !!x && x.kind === 'link-location';
     }
 
+    export function areLocationsEqual(locA: Location, locB: Location) {
+        return (
+            locA.aIndex === locB.aIndex && locA.bIndex === locB.bIndex &&
+            locA.aUnit.id === locB.aUnit.id && locA.bUnit.id === locB.bUnit.id
+        )
+    }
+
     export interface Loci {
         readonly kind: 'link-loci',
         readonly links: ReadonlyArray<Location>
@@ -45,6 +52,14 @@ namespace Link {
         return !!x && x.kind === 'link-loci';
     }
 
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.links.length !== b.links.length) return false
+        for (let i = 0, il = a.links.length; i < il; ++i) {
+            if (!areLocationsEqual(a.links[i], b.links[i])) return false
+        }
+        return true
+    }
+
     export function getType(structure: Structure, link: Location<Unit.Atomic>): LinkType {
         if (link.aUnit === link.bUnit) {
             const links = link.aUnit.links;

+ 5 - 0
src/mol-model/structure/structure/unit/links/data.ts

@@ -18,10 +18,13 @@ namespace IntraUnitLinks {
 }
 
 class InterUnitBonds {
+    /** Number of inter-unit bonds */
     readonly bondCount: number
+    /** Array of inter-unit bonds */
     readonly bonds: ReadonlyArray<InterUnitBonds.Bond>
     private readonly bondKeyIndex: Map<string, number>
 
+    /** Get an array of unit-pair-bonds that are linked to the given unit */
     getLinkedUnits(unit: Unit): ReadonlyArray<InterUnitBonds.UnitPairBonds> {
         if (!this.map.has(unit.id)) return emptyArray;
         return this.map.get(unit.id)!;
@@ -34,11 +37,13 @@ class InterUnitBonds {
         return index !== undefined ? index : -1
     }
 
+    /** Get inter-unit bond given a pair of indices and units */
     getBond(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): InterUnitBonds.Bond | undefined {
         const index = this.getBondIndex(indexA, unitA, indexB, unitB)
         return index !== -1 ? this.bonds[index] : undefined
     }
 
+    /** Get inter-unit bond given a link-location */
     getBondFromLocation(l: Link.Location) {
         return this.getBond(l.aIndex, l.aUnit, l.bIndex, l.bUnit);
     }

+ 86 - 47
src/mol-model/structure/structure/util/boundary.ts

@@ -2,66 +2,105 @@
  * Copyright (c) 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 Structure from '../structure'
-import { Box3D, Sphere3D } from 'mol-math/geometry';
+import Unit from '../unit';
+import { Box3D, Sphere3D, SymmetryOperator } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { SortedArray } from 'mol-data/int';
+import { ElementIndex } from '../../model/indexing';
 
-function computeStructureBoundary(s: Structure): { box: Box3D, sphere: Sphere3D } {
-    const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
-    const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];
+export type Boundary = { box: Box3D, sphere: Sphere3D }
 
-    const { units } = s;
+function computeElementsPositionBoundary(elements: SortedArray<ElementIndex>, position: SymmetryOperator.CoordinateMapper): Boundary {
+    const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+    const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+    const center = Vec3.zero()
 
-    let cx = 0, cy = 0, cz = 0;
-    let radiusSq = 0;
-    let size = 0;
+    let radiusSq = 0
+    let size = 0
 
-    for (let i = 0, _i = units.length; i < _i; i++) {
-        const { x, y, z } = units[i].conformation;
-
-        const elements = units[i].elements;
-        size += elements.length;
-        for (let j = 0, _j = elements.length; j < _j; j++) {
-            const e = elements[j];
-            const xx = x(e), yy = y(e), zz = z(e);
-
-            min[0] = Math.min(xx, min[0]);
-            min[1] = Math.min(yy, min[1]);
-            min[2] = Math.min(zz, min[2]);
-            max[0] = Math.max(xx, max[0]);
-            max[1] = Math.max(yy, max[1]);
-            max[2] = Math.max(zz, max[2]);
-
-            cx += xx;
-            cy += yy;
-            cz += zz;
-        }
-    }
+    const p = Vec3.zero()
 
-    if (size > 0) {
-        cx /= size;
-        cy /= size;
-        cz /= size;
+    size += elements.length
+    for (let j = 0, _j = elements.length; j < _j; j++) {
+        position(elements[j], p)
+        Vec3.min(min, min, p)
+        Vec3.max(max, max, p)
+        Vec3.add(center, center, p)
     }
 
-    for (let i = 0, _i = units.length; i < _i; i++) {
-        const { x, y, z } = units[i].conformation;
-
-        const elements = units[i].elements;
-        for (let j = 0, _j = elements.length; j < _j; j++) {
-            const e = elements[j];
-            const dx = x(e) - cx, dy = y(e) - cy, dz = z(e) - cz;
-            const d = dx * dx + dy * dy + dz * dz;
-            if (d > radiusSq) radiusSq = d;
-        }
+    if (size > 0) Vec3.scale(center, center, 1/size)
+
+    for (let j = 0, _j = elements.length; j < _j; j++) {
+        position(elements[j], p)
+        const d = Vec3.squaredDistance(p, center)
+        if (d > radiusSq) radiusSq = d
     }
 
     return {
-        box: { min: Vec3.ofArray(min), max: Vec3.ofArray(max) },
-        sphere: { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) }
-    };
+        box: { min, max },
+        sphere: { center, radius: Math.sqrt(radiusSq) }
+    }
 }
 
-export { computeStructureBoundary }
+function computeInvariantUnitBoundary(u: Unit): Boundary {
+    return computeElementsPositionBoundary(u.elements, u.conformation.invariantPosition)
+}
+
+export function computeUnitBoundary(u: Unit): Boundary {
+    return computeElementsPositionBoundary(u.elements, u.conformation.position)
+}
+
+const tmpBox = Box3D.empty()
+const tmpSphere = Sphere3D.zero()
+
+export function computeStructureBoundary(s: Structure): Boundary {
+    const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+    const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+    const center = Vec3.zero()
+
+    const { units } = s
+
+    const boundaryMap: Map<number, Boundary> = new Map()
+    function getInvariantBoundary(u: Unit) {
+        let boundary: Boundary
+        if (boundaryMap.has(u.invariantId)) {
+            boundary = boundaryMap.get(u.invariantId)!
+        } else {
+            boundary = computeInvariantUnitBoundary(u)
+            boundaryMap.set(u.invariantId, boundary)
+        }
+        return boundary
+    }
+
+    let radius = 0
+    let size = 0
+
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        const u = units[i]
+        const invariantBoundary = getInvariantBoundary(u)
+        const m = u.conformation.operator.matrix
+        size += u.elements.length
+        Box3D.transform(tmpBox, invariantBoundary.box, m)
+        Vec3.min(min, min, tmpBox.min)
+        Vec3.max(max, max, tmpBox.max)
+        Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m)
+        Vec3.scaleAndAdd(center, center, tmpSphere.center, u.elements.length)
+    }
+
+    if (size > 0) Vec3.scale(center, center, 1/size)
+
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        const u = units[i]
+        const invariantBoundary = getInvariantBoundary(u)
+        const m = u.conformation.operator.matrix
+        Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m)
+        const d = Vec3.distance(tmpSphere.center, center) + tmpSphere.radius
+        if (d > radius) radius = d
+    }
+
+    return { box: { min, max }, sphere: { center, radius } }
+}

+ 35 - 0
src/mol-model/structure/structure/util/nucleotide.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, SortedArray } from 'mol-data/int';
+import { isNucleic, MoleculeType } from 'mol-model/structure/model/types';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+export function getNucleotideElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { chemicalComponentMap } = model.properties
+    const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
+    const { label_comp_id } = residues
+    const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+    while (chainIt.hasNext) {
+        residueIt.setSegment(chainIt.move());
+
+        while (residueIt.hasNext) {
+            const { index } = residueIt.move();
+            const cc = chemicalComponentMap.get(label_comp_id.value(index))
+            const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown
+
+            if (isNucleic(moleculeType)) {
+                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
+            }
+        }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}

+ 75 - 0
src/mol-model/structure/structure/util/polymer.ts

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, OrderedSet, Interval, SortedArray } from 'mol-data/int';
+import SortedRanges from 'mol-data/int/sorted-ranges';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+export function getAtomicPolymerElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const polymerIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.polymerRanges, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+    while (polymerIt.hasNext) {
+        const polymerSegment = polymerIt.move()
+        residueIt.setSegment(polymerSegment)
+        while (residueIt.hasNext) {
+            const residueSegment = residueIt.move()
+            const { start, end, index } = residueSegment
+            if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) {
+                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
+            }
+        }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarsePolymerElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const polymerRanges = Unit.isSpheres(unit) ? spheres.polymerRanges : gaussians.polymerRanges
+    const polymerIt = SortedRanges.transientSegments(polymerRanges, elements)
+    while (polymerIt.hasNext) {
+        const { start, end } = polymerIt.move()
+        for (let i = start; i < end; ++i) { indices.push(elements[i]) }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getAtomicGapElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model, residueIndex } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const gapIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.gapRanges, unit.elements);
+    while (gapIt.hasNext) {
+        const gapSegment = gapIt.move();
+        const indexStart = residueIndex[elements[gapSegment.start]]
+        const indexEnd = residueIndex[elements[gapSegment.end - 1]]
+        const elementIndexStart = getElementIndexForAtomRole(model, indexStart, 'trace')
+        const elementIndexEnd = getElementIndexForAtomRole(model, indexEnd, 'trace')
+        indices.push(elementIndexStart === -1 ? residueAtomSegments.offsets[indexStart] : elementIndexStart)
+        indices.push(elementIndexEnd === -1 ? residueAtomSegments.offsets[indexEnd] : elementIndexEnd)
+
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarseGapElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const gapRanges = Unit.isSpheres(unit) ? spheres.gapRanges : gaussians.gapRanges
+    const gapIt = SortedRanges.transientSegments(gapRanges, elements)
+    while (gapIt.hasNext) {
+        const { start, end } = gapIt.move()
+        indices.push(elements[start], elements[end - 1])
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}

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