Browse Source

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

David Sehnal 6 years ago
parent
commit
1944457a2c
74 changed files with 1220 additions and 386 deletions
  1. 0 1
      package.json
  2. 5 6
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  3. 5 5
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  4. 6 0
      src/mol-geo/geometry/direct-volume/transfer-function.ts
  5. 20 20
      src/mol-geo/geometry/mesh/builder/bounding-box.ts
  6. 7 7
      src/mol-geo/geometry/mesh/builder/cylinder.ts
  7. 10 10
      src/mol-geo/geometry/mesh/builder/sheet.ts
  8. 2 2
      src/mol-geo/geometry/mesh/builder/sphere.ts
  9. 2 2
      src/mol-geo/geometry/mesh/builder/tube.ts
  10. 50 57
      src/mol-geo/geometry/mesh/mesh-builder.ts
  11. 16 6
      src/mol-model-props/rcsb/assembly-symmetry.ts
  12. 117 0
      src/mol-model-props/rcsb/themes/assembly-symmetry.ts
  13. 1 1
      src/mol-model/structure/query/queries/generators.ts
  14. 4 4
      src/mol-model/structure/query/queries/internal.ts
  15. 1 1
      src/mol-model/structure/query/selection.ts
  16. 2 2
      src/mol-model/structure/query/utils/structure-set.ts
  17. 16 8
      src/mol-model/structure/structure/structure.ts
  18. 3 3
      src/mol-model/structure/structure/symmetry.ts
  19. 5 9
      src/mol-model/structure/structure/util/nucleotide.ts
  20. 5 4
      src/mol-model/structure/structure/util/polymer.ts
  21. 1 1
      src/mol-model/structure/structure/util/subset-builder.ts
  22. 1 1
      src/mol-model/structure/structure/util/unique-subset-builder.ts
  23. 0 5
      src/mol-model/structure/util.ts
  24. 3 75
      src/mol-plugin/behavior/dynamic/custom-props.ts
  25. 79 0
      src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts
  26. 50 0
      src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts
  27. 2 2
      src/mol-plugin/context.ts
  28. 2 1
      src/mol-plugin/index.ts
  29. 64 0
      src/mol-plugin/skin/base/components/line-graph.scss
  30. 2 1
      src/mol-plugin/skin/base/ui.scss
  31. 10 12
      src/mol-plugin/state/transforms/representation.ts
  32. 354 0
      src/mol-plugin/ui/controls/line-graph/line-graph-component.tsx
  33. 47 0
      src/mol-plugin/ui/controls/line-graph/point-component.tsx
  34. 65 3
      src/mol-plugin/ui/controls/parameters.tsx
  35. 1 1
      src/mol-plugin/ui/state.tsx
  36. 5 0
      src/mol-repr/representation.ts
  37. 1 1
      src/mol-repr/structure/complex-visual.ts
  38. 12 2
      src/mol-repr/structure/representation/cartoon.ts
  39. 1 1
      src/mol-repr/structure/representation/distance-restraint.ts
  40. 1 1
      src/mol-repr/structure/units-visual.ts
  41. 21 21
      src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
  42. 23 17
      src/mol-repr/structure/visual/nucleotide-block-mesh.ts
  43. 6 6
      src/mol-repr/structure/visual/polymer-backbone-cylinder.ts
  44. 4 4
      src/mol-repr/structure/visual/polymer-direction-wedge.ts
  45. 8 8
      src/mol-repr/structure/visual/polymer-gap-cylinder.ts
  46. 5 5
      src/mol-repr/structure/visual/polymer-trace-mesh.ts
  47. 4 4
      src/mol-repr/structure/visual/util/element.ts
  48. 7 7
      src/mol-repr/structure/visual/util/link.ts
  49. 3 3
      src/mol-repr/structure/visual/util/nucleotide.ts
  50. 3 3
      src/mol-repr/structure/visual/util/polymer.ts
  51. 5 11
      src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts
  52. 4 9
      src/mol-repr/structure/visual/util/polymer/gap-iterator.ts
  53. 10 4
      src/mol-theme/color.ts
  54. 2 1
      src/mol-theme/color/carbohydrate-symbol.ts
  55. 2 1
      src/mol-theme/color/chain-id.ts
  56. 2 1
      src/mol-theme/color/cross-link.ts
  57. 2 1
      src/mol-theme/color/element-index.ts
  58. 2 1
      src/mol-theme/color/element-symbol.ts
  59. 2 1
      src/mol-theme/color/molecule-type.ts
  60. 2 1
      src/mol-theme/color/polymer-id.ts
  61. 2 1
      src/mol-theme/color/polymer-index.ts
  62. 2 1
      src/mol-theme/color/residue-name.ts
  63. 2 1
      src/mol-theme/color/secondary-structure.ts
  64. 2 1
      src/mol-theme/color/sequence-id.ts
  65. 2 1
      src/mol-theme/color/shape-group.ts
  66. 2 1
      src/mol-theme/color/uniform.ts
  67. 2 1
      src/mol-theme/color/unit-index.ts
  68. 12 6
      src/mol-theme/size.ts
  69. 2 1
      src/mol-theme/size/physical.ts
  70. 2 1
      src/mol-theme/size/uniform.ts
  71. 7 4
      src/mol-util/data-source.ts
  72. 85 0
      src/mol-util/graphql-client.ts
  73. 2 2
      src/mol-util/param-definition.ts
  74. 1 1
      src/servers/model/properties/providers/rcsb.ts

+ 0 - 1
package.json

@@ -117,7 +117,6 @@
     "compression": "^1.7.3",
     "express": "^4.16.4",
     "graphql": "^14.0.2",
-    "graphql-request": "^1.8.2",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
     "react": "^16.6.3",

+ 5 - 6
src/mol-canvas3d/helper/bounding-sphere-helper.ts

@@ -17,24 +17,23 @@ export class BoundingSphereHelper {
     private renderObject: MeshRenderObject
 
     constructor(private scene: Scene, visible: boolean) {
-        const builder = MeshBuilder.create(1024, 512)
-        this.mesh = builder.getMesh()
+        this.mesh = MeshBuilder.getMesh(MeshBuilder.createState(1024, 512))
         const values = Mesh.createValuesSimple(this.mesh, { alpha: 0.1, doubleSided: false })
         this.renderObject = createMeshRenderObject(values, { visible, pickable: false, opaque: false })
         scene.add(this.renderObject)
     }
 
     update() {
-        const builder = MeshBuilder.create(1024, 512, this.mesh)
+        const builderState = MeshBuilder.createState(1024, 512, this.mesh)
         if (this.scene.boundingSphere.radius) {
-            addSphere(builder, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 2)
+            addSphere(builderState, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 2)
         }
         this.scene.forEach(r => {
             if (r.boundingSphere.radius) {
-                addSphere(builder, r.boundingSphere.center, r.boundingSphere.radius, 2)
+                addSphere(builderState, r.boundingSphere.center, r.boundingSphere.radius, 2)
             }
         })
-        this.mesh = builder.getMesh()
+        this.mesh = MeshBuilder.getMesh(builderState)
         ValueCell.update(this.renderObject.values.drawCount, Geometry.getDrawCount(this.mesh))
     }
 

+ 5 - 5
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -8,9 +8,9 @@ import { ValueCell } from 'mol-util'
 import { Sphere3D, Box3D } from 'mol-math/geometry'
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { DirectVolumeValues } from 'mol-gl/renderable/direct-volume';
-import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { Vec3, Mat4, Vec2 } from 'mol-math/linear-algebra';
 import { Box } from '../../primitive/box';
-import { getControlPointsFromString, createTransferFunctionTexture } from './transfer-function';
+import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './transfer-function';
 import { Texture } from 'mol-gl/webgl/texture';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { TransformData } from '../transform-data';
@@ -72,7 +72,7 @@ export namespace DirectVolume {
         ...Geometry.Params,
         isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
         renderMode: PD.Select('isosurface', RenderModeOptions),
-        controlPoints: PD.Text('0.19:0.1, 0.2:0.5, 0.21:0.1, 0.4:0.3'),
+        controlPoints: PD.LineGraph([Vec2.create(0.19, 0.1), Vec2.create(0.2, 0.5), Vec2.create(0.21, 0.1), Vec2.create(0.4, 0.3)]),
     }
     export type Params = typeof Params
 
@@ -93,7 +93,7 @@ export namespace DirectVolume {
             transform.aTransform.ref.value, transform.instanceCount.ref.value
         )
 
-        const controlPoints = getControlPointsFromString(props.controlPoints)
+        const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
         const transferTex = createTransferFunctionTexture(controlPoints)
 
         const maxSteps = Math.ceil(Vec3.magnitude(gridDimension.ref.value)) * 2
@@ -130,7 +130,7 @@ export namespace DirectVolume {
         ValueCell.updateIfChanged(values.dUseFog, props.useFog)
         ValueCell.updateIfChanged(values.dRenderMode, props.renderMode)
 
-        const controlPoints = getControlPointsFromString(props.controlPoints)
+        const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
         createTransferFunctionTexture(controlPoints, values.tTransferTex)
     }
 

+ 6 - 0
src/mol-geo/geometry/direct-volume/transfer-function.ts

@@ -9,6 +9,7 @@ import { spline } from 'mol-math/interpolate';
 import { ColorScale } from 'mol-util/color';
 import { ColorMatplotlib } from 'mol-util/color/tables';
 import { ValueCell } from 'mol-util';
+import { Vec2 } from 'mol-math/linear-algebra';
 
 export interface ControlPoint { x: number, alpha: number }
 
@@ -18,6 +19,11 @@ export function getControlPointsFromString(s: string): ControlPoint[] {
         return { x: parseFloat(ps[0]), alpha: parseFloat(ps[1]) }
     })
 }
+
+export function getControlPointsFromVec2Array(array: Vec2[]): ControlPoint[] {
+    return array.map(v => ({ x: v[0], alpha: v[1] }))
+}
+
 // TODO move core function to mol-canvas3d/color
 export function createTransferFunctionTexture(controlPoints: ControlPoint[], texture?: ValueCell<TextureImage<Uint8Array>>): ValueCell<TextureImage<Uint8Array>> {
     const cp = [

+ 20 - 20
src/mol-geo/geometry/mesh/builder/bounding-box.ts

@@ -15,7 +15,7 @@ const tmpStart = Vec3.zero()
 const tmpEnd = Vec3.zero()
 const cylinderProps: CylinderProps = {}
 
-export function addBoundingBox(builder: MeshBuilder, box: Box3D, radius: number, detail: number, radialSegments: number) {
+export function addBoundingBox(state: MeshBuilder.State, box: Box3D, radius: number, detail: number, radialSegments: number) {
     const { min, max } = box
 
     cylinderProps.radiusTop = radius
@@ -23,43 +23,43 @@ export function addBoundingBox(builder: MeshBuilder, box: Box3D, radius: number,
     cylinderProps.radialSegments = radialSegments
 
     Vec3.set(tmpStart, max[0], max[1], max[2])
-    addSphere(builder, tmpStart, radius, detail)
+    addSphere(state, tmpStart, radius, detail)
     Vec3.set(tmpEnd, max[0], max[1], min[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, max[0], min[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, min[0], max[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
 
     Vec3.set(tmpStart, min[0], min[1], min[2])
-    addSphere(builder, tmpStart, radius, detail)
+    addSphere(state, tmpStart, radius, detail)
     Vec3.set(tmpEnd, min[0], min[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, min[0], max[1], min[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, max[0], min[1], min[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
 
     Vec3.set(tmpStart, max[0], min[1], min[2])
-    addSphere(builder, tmpStart, radius, detail)
+    addSphere(state, tmpStart, radius, detail)
     Vec3.set(tmpEnd, max[0], min[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, max[0], max[1], min[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
 
     Vec3.set(tmpStart, min[0], min[1], max[2])
-    addSphere(builder, tmpStart, radius, detail)
+    addSphere(state, tmpStart, radius, detail)
     Vec3.set(tmpEnd, min[0], max[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, max[0], min[1], max[2])
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
 
     Vec3.set(tmpStart, min[0], max[1], min[2])
-    addSphere(builder, tmpStart, radius, detail)
+    addSphere(state, tmpStart, radius, detail)
     Vec3.set(tmpEnd, max[0], max[1], min[2])
-    addSphere(builder, tmpEnd, radius, detail)
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addSphere(state, tmpEnd, radius, detail)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
     Vec3.set(tmpEnd, min[0], max[1], max[2])
-    addSphere(builder, tmpEnd, radius, detail)
-    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    addSphere(state, tmpEnd, radius, detail)
+    addCylinder(state, tmpStart, tmpEnd, 1, cylinderProps)
 }

+ 7 - 7
src/mol-geo/geometry/mesh/builder/cylinder.ts

@@ -40,15 +40,15 @@ function getCylinder(props: CylinderProps) {
     return cylinder
 }
 
-export function addCylinder(builder: MeshBuilder, start: Vec3, end: Vec3, lengthScale: number, props: CylinderProps) {
+export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: CylinderProps) {
     const d = Vec3.distance(start, end) * lengthScale
     props.height = d
     Vec3.sub(tmpCylinderDir, end, start)
     setCylinderMat(tmpCylinderMat, start, tmpCylinderDir, d)
-    builder.add(tmpCylinderMat, getCylinder(props))
+    MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props))
 }
 
-export function addDoubleCylinder(builder: MeshBuilder, start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps) {
+export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps) {
     const d = Vec3.distance(start, end) * lengthScale
     props.height = d
     const cylinder = getCylinder(props)
@@ -56,14 +56,14 @@ export function addDoubleCylinder(builder: MeshBuilder, start: Vec3, end: Vec3,
     // positivly shifted cylinder
     Vec3.add(tmpCylinderStart, start, shift)
     setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d)
-    builder.add(tmpCylinderMat, cylinder)
+    MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder)
     // negativly shifted cylinder
     Vec3.sub(tmpCylinderStart, start, shift)
     setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d)
-    builder.add(tmpCylinderMat, cylinder)
+    MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder)
 }
 
-export function addFixedCountDashedCylinder(builder: MeshBuilder, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps) {
+export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps) {
     const s = Math.floor(segmentCount / 2)
     const step = 1 / segmentCount
 
@@ -84,6 +84,6 @@ export function addFixedCountDashedCylinder(builder: MeshBuilder, start: Vec3, e
         Vec3.setMagnitude(tmpCylinderDir, tmpCylinderDir, d * f)
         Vec3.add(tmpCylinderStart, start, tmpCylinderDir)
         setCylinderMat(tmpCylinderMat, tmpCylinderStart, tmpCylinderDir, d * step)
-        builder.add(tmpCylinderMat, cylinder)
+        MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder)
     }
 }

+ 10 - 10
src/mol-geo/geometry/mesh/builder/sheet.ts

@@ -27,8 +27,8 @@ const p2 = Vec3.zero()
 const p3 = Vec3.zero()
 const p4 = Vec3.zero()
 
-function addCap(offset: number, builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
-    const { vertices, normals, indices } = builder.state
+function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
+    const { vertices, normals, indices } = state
     const vertexCount = vertices.elementCount
 
     Vec3.fromArray(verticalLeftVector, normalVectors, offset)
@@ -62,8 +62,8 @@ function addCap(offset: number, builder: MeshBuilder, controlPoints: ArrayLike<n
 }
 
 /** set arrowHeight = 0 for no arrow */
-export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) {
-    const { currentGroup, vertices, normals, indices, groups } = builder.state
+export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) {
+    const { currentGroup, vertices, normals, indices, groups } = state
 
     let vertexCount = vertices.elementCount
     let offsetLength = 0
@@ -150,18 +150,18 @@ export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>,
 
     if (startCap) {
         const h = arrowHeight === 0 ? height : arrowHeight
-        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, h, h)
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h)
     } else if (arrowHeight > 0) {
-        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
-        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height)
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height)
     }
 
     if (endCap && arrowHeight === 0) {
-        addCap(linearSegments * 3, builder, controlPoints, normalVectors, binormalVectors, width, height, height)
+        addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, height, height)
     }
 
-    const addedVertexCount = (linearSegments + 1) * 8 + 
-        (startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) + 
+    const addedVertexCount = (linearSegments + 1) * 8 +
+        (startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) +
         (endCap && arrowHeight === 0 ? 4 : 0)
     for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup)
 }

+ 2 - 2
src/mol-geo/geometry/mesh/builder/sphere.ts

@@ -25,6 +25,6 @@ function getSphere(detail: number) {
     return sphere
 }
 
-export function addSphere(builder: MeshBuilder, center: Vec3, radius: number, detail: number) {
-    builder.add(setSphereMat(tmpSphereMat, center, radius), getSphere(detail))
+export function addSphere(state: MeshBuilder.State, center: Vec3, radius: number, detail: number) {
+    MeshBuilder.addPrimitive(state, setSphereMat(tmpSphereMat, center, radius), getSphere(detail))
 }

+ 2 - 2
src/mol-geo/geometry/mesh/builder/tube.ts

@@ -27,8 +27,8 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
     out[2] = (a[2] * sa) + (b[2] * sb) + c[2];
 }
 
-export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean) {
-    const { currentGroup, vertices, normals, indices, groups } = builder.state
+export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean) {
+    const { currentGroup, vertices, normals, indices, groups } = state
 
     let vertexCount = vertices.elementCount
     const di = 1 / linearSegments

+ 50 - 57
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -11,72 +11,65 @@ import { Mesh } from './mesh';
 import { getNormalMatrix } from '../../util';
 import { Primitive } from '../../primitive/primitive';
 
-export interface MeshBuilderState {
-    readonly currentGroup: number
-    readonly vertices: ChunkedArray<number, 3>
-    readonly normals: ChunkedArray<number, 3>
-    readonly indices: ChunkedArray<number, 3>
-    readonly groups: ChunkedArray<number, 1>
-}
-
-export interface MeshBuilder {
-    state: MeshBuilderState
-    add(t: Mat4, primitive: Primitive): void
-    setGroup(id: number): void
-    getMesh(): Mesh
-}
-
 const tmpV = Vec3.zero()
 const tmpMat3 = Mat3.zero()
 
 export namespace MeshBuilder {
-    export function create(initialCount = 2048, chunkSize = 1024, mesh?: Mesh): MeshBuilder {
-        const vertices = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.vertexBuffer.ref.value : initialCount);
-        const normals = ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.normalBuffer.ref.value : initialCount);
-        const indices = ChunkedArray.create(Uint32Array, 3, chunkSize * 3, mesh ? mesh.indexBuffer.ref.value : initialCount * 3);
-        const groups = ChunkedArray.create(Float32Array, 1, chunkSize, mesh ? mesh.groupBuffer.ref.value : initialCount);
+    export interface State {
+        currentGroup: number
+        readonly vertices: ChunkedArray<number, 3>
+        readonly normals: ChunkedArray<number, 3>
+        readonly indices: ChunkedArray<number, 3>
+        readonly groups: ChunkedArray<number, 1>
+        readonly mesh?: Mesh
+    }
 
-        let currentGroup = -1
+    export function createState(initialCount = 2048, chunkSize = 1024, mesh?: Mesh): State {
+        return {
+            currentGroup: -1,
+            vertices: ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.vertexBuffer.ref.value : initialCount),
+            normals: ChunkedArray.create(Float32Array, 3, chunkSize, mesh ? mesh.normalBuffer.ref.value : initialCount),
+            indices: ChunkedArray.create(Uint32Array, 3, chunkSize * 3, mesh ? mesh.indexBuffer.ref.value : initialCount * 3),
+            groups: ChunkedArray.create(Float32Array, 1, chunkSize, mesh ? mesh.groupBuffer.ref.value : initialCount),
+            mesh
+        }
+    }
 
-        function add(t: Mat4, primitive: Primitive) {
-            const { vertices: va, normals: na, indices: ia } = primitive
-            const offset = vertices.elementCount
-            const n = getNormalMatrix(tmpMat3, t)
-            for (let i = 0, il = va.length; i < il; i += 3) {
-                // position
-                Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t)
-                ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
-                // normal
-                Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n)
-                ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);
-                // group
-                ChunkedArray.add(groups, currentGroup);
-            }
-            for (let i = 0, il = ia.length; i < il; i += 3) {
-                ChunkedArray.add3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
-            }
+    export function addPrimitive(state: State, t: Mat4, primitive: Primitive) {
+        const { vertices: va, normals: na, indices: ia } = primitive
+        const { vertices, normals, indices, groups, currentGroup } = state
+        const offset = vertices.elementCount
+        const n = getNormalMatrix(tmpMat3, t)
+        for (let i = 0, il = va.length; i < il; i += 3) {
+            // position
+            Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t)
+            ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
+            // normal
+            Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n)
+            ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);
+            // group
+            ChunkedArray.add(groups, currentGroup);
         }
+        for (let i = 0, il = ia.length; i < il; i += 3) {
+            ChunkedArray.add3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
+        }
+    }
 
+    export function getMesh (state: State): Mesh {
+        const { vertices, normals, indices, groups, mesh } = state
+        const vb = ChunkedArray.compact(vertices, true) as Float32Array
+        const ib = ChunkedArray.compact(indices, true) as Uint32Array
+        const nb = ChunkedArray.compact(normals, true) as Float32Array
+        const gb = ChunkedArray.compact(groups, true) as Float32Array
         return {
-            state: { get currentGroup() { return currentGroup }, vertices, normals, indices, groups },
-            add,
-            setGroup: (group: number) => { currentGroup = group },
-            getMesh: () => {
-                const vb = ChunkedArray.compact(vertices, true) as Float32Array
-                const ib = ChunkedArray.compact(indices, true) as Uint32Array
-                const nb = ChunkedArray.compact(normals, true) as Float32Array
-                const gb = ChunkedArray.compact(groups, true) as Float32Array
-                return {
-                    kind: 'mesh',
-                    vertexCount: vertices.elementCount,
-                    triangleCount: indices.elementCount,
-                    vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
-                    indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
-                    normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
-                    groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
-                    normalsComputed: true,
-                }
-            }
+            kind: 'mesh',
+            vertexCount: state.vertices.elementCount,
+            triangleCount: state.indices.elementCount,
+            vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb),
+            indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib),
+            normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb),
+            groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb),
+            normalsComputed: true,
         }
     }
 }

+ 16 - 6
src/mol-model-props/rcsb/symmetry.ts → src/mol-model-props/rcsb/assembly-symmetry.ts

@@ -4,8 +4,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { GraphQLClient } from 'graphql-request'
-
 import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types';
 import query from './graphql/symmetry.gql';
 
@@ -18,6 +16,9 @@ import { CifExportContext } from 'mol-model/structure/export/mmcif';
 import { toTable } from 'mol-io/reader/cif/schema';
 import { CifCategory } from 'mol-io/reader/cif';
 import { PropertyWrapper } from 'mol-model-props/common/wrapper';
+import { Task, RuntimeContext } from 'mol-task';
+import { GraphQLClient } from 'mol-util/graphql-client';
+import { ajaxGet } from 'mol-util/data-source';
 
 const { str, int, float, Aliased, Vector, List } = Column.Schema;
 
@@ -152,8 +153,6 @@ const _Descriptor: ModelPropertyDescriptor = {
     }
 }
 
-const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql')
-
 export interface AssemblySymmetry {
     db: AssemblySymmetry.Database
     getSymmetries(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry']>
@@ -177,7 +176,10 @@ export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetr
     }
 }
 
+const Client = new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, (url: string, type: 'string' | 'binary', body?: string) => ajaxGet({ url, type, body }) )
+
 export namespace AssemblySymmetry {
+    export const GraphQLEndpointURL = 'http://rest-experimental.rcsb.org/graphql'
     export const Schema = {
         rcsb_assembly_symmetry_info: {
             updated_datetime_utc: Column.Schema.str
@@ -247,7 +249,7 @@ export namespace AssemblySymmetry {
 
     export const Descriptor = _Descriptor;
 
-    export async function attachFromCifOrAPI(model: Model) {
+    export async function attachFromCifOrAPI(model: Model, client: GraphQLClient = Client, ctx?: RuntimeContext) {
         if (model.customProperties.has(Descriptor)) return true;
 
         let db: Database
@@ -258,7 +260,7 @@ export namespace AssemblySymmetry {
             let result: AssemblySymmetryGraphQL.Query
             const variables: AssemblySymmetryGraphQL.Variables = { pdbId: model.label.toLowerCase() };
             try {
-                result = await client.request<AssemblySymmetryGraphQL.Query>(query, variables);
+                result = await client.request<AssemblySymmetryGraphQL.Query>(ctx || RuntimeContext.Synchronous, query, variables);
             } catch (e) {
                 console.error(e)
                 return false;
@@ -273,6 +275,14 @@ export namespace AssemblySymmetry {
         return true;
     }
 
+    export function createAttachTask(fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
+        return (model: Model) => Task.create('RCSB Assembly Symmetry', async ctx => {
+            if (get(model)) return true;
+
+            return await attachFromCifOrAPI(model, new GraphQLClient(AssemblySymmetry.GraphQLEndpointURL, fetch), ctx)
+        });
+    }
+
     export function get(model: Model): AssemblySymmetry | undefined {
         return model._staticPropertyData.__RCSBAssemblySymmetry__;
     }

+ 117 - 0
src/mol-model-props/rcsb/themes/assembly-symmetry.ts

@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ThemeDataContext } from 'mol-theme/theme';
+import { ColorTheme, LocationColor } from 'mol-theme/color';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { Table } from 'mol-data/db';
+import { AssemblySymmetry } from '../assembly-symmetry';
+import { ColorScale, Color } from 'mol-util/color';
+import { Unit, StructureElement, StructureProperties } from 'mol-model/structure';
+import { Location } from 'mol-model/location';
+import { ColorListName, ColorListOptions } from 'mol-util/color/scale';
+
+const DefaultColor = Color(0xCCCCCC)
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.label_asym_id
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}
+
+function clusterMemberKey (asym_id: string, oper_list_ids: string[]) {
+    return `${asym_id}-${oper_list_ids.join('x')}`
+}
+
+export const AssemblySymmetryClusterColorThemeParams = {
+    list: PD.Select<ColorListName>('Viridis', ColorListOptions),
+    symmetryId: PD.Select<number>(0, []),
+}
+export type AssemblySymmetryClusterColorThemeParams = typeof AssemblySymmetryClusterColorThemeParams
+export function getAssemblySymmetryClusterColorThemeParams(ctx: ThemeDataContext) {
+    const params = PD.clone(AssemblySymmetryClusterColorThemeParams)
+
+    if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
+        const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
+        const assemblyName = ctx.structure.assemblyName
+        const s = assemblySymmetry.db.rcsb_assembly_symmetry
+        if (s._rowCount) {
+            params.symmetryId.options = []
+            for (let i = 0, il = s._rowCount; i < il; ++i) {
+                if (s.assembly_id.value(i) === assemblyName) {
+                    params.symmetryId.options.push([
+                        s.id.value(i), `${s.symbol.value(i)} ${s.kind.value(i)}`
+                    ])
+                }
+            }
+            params.symmetryId.defaultValue = s.id.value(0)
+        }
+    }
+
+    return params
+}
+
+export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props: PD.Values<AssemblySymmetryClusterColorThemeParams>): ColorTheme<AssemblySymmetryClusterColorThemeParams> {
+    let color: LocationColor = () => DefaultColor
+
+    const { symmetryId } = props
+
+    if (ctx.structure && ctx.structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)) {
+        const assemblySymmetry = AssemblySymmetry.get(ctx.structure.models[0])!
+
+        const s = assemblySymmetry.db.rcsb_assembly_symmetry
+        const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId)
+        if (symmetry) {
+
+            const clusters = assemblySymmetry.getClusters(symmetryId)
+            if (clusters._rowCount) {
+
+                const clusterByMember = new Map<string, number>()
+                for (let i = 0, il = clusters._rowCount; i < il; ++i) {
+                    const clusterMembers = assemblySymmetry.getClusterMembers(clusters.id.value(i))
+                    for (let j = 0, jl = clusterMembers._rowCount; j < jl; ++j) {
+                        const asym_id = clusterMembers.asym_id.value(j)
+                        const oper_list_ids = clusterMembers.pdbx_struct_oper_list_ids.value(j)
+                        if (oper_list_ids.length === 0) oper_list_ids.push('1') // TODO hack assuming '1' is the id of the identity operator
+                        clusterByMember.set(clusterMemberKey(asym_id, oper_list_ids), i)
+                    }
+                }
+
+                const scale = ColorScale.create({ listOrName: props.list, domain: [ 0, clusters._rowCount - 1 ] })
+
+                color = (location: Location): Color => {
+                    if (StructureElement.isLocation(location)) {
+                        const asym_id = getAsymId(location.unit)
+                        const ns = location.unit.conformation.operator.name.split('-')
+                        const oper_list_ids = ns.length === 2 ? ns[1].split('x') : []
+                        const cluster = clusterByMember.get(clusterMemberKey(asym_id(location), oper_list_ids))
+                        return cluster !== undefined ? scale.color(cluster) : DefaultColor
+                    }
+                    return DefaultColor
+                }
+            }
+        }
+    }
+
+    return {
+        factory: AssemblySymmetryClusterColorTheme,
+        granularity: 'instance',
+        color,
+        props,
+        description: 'Assigns chain colors according to assembly symmetry cluster membership.',
+    }
+}
+
+export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams> = {
+    label: 'RCSB Assembly Symmetry Cluster',
+    factory: AssemblySymmetryClusterColorTheme,
+    getParams: getAssemblySymmetryClusterColorThemeParams,
+    defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams)
+}

+ 1 - 1
src/mol-model/structure/query/queries/generators.ts

@@ -171,7 +171,7 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group
 function getRingStructure(unit: Unit.Atomic, ring: UnitRing) {
     const elements = new Int32Array(ring.length) as any as ElementIndex[];
     for (let i = 0, _i = ring.length; i < _i; i++) elements[i] = unit.elements[ring[i]];
-    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))])
+    return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))], '')
 }
 
 export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): StructureQuery {

+ 4 - 4
src/mol-model/structure/query/queries/internal.ts

@@ -35,7 +35,7 @@ export function atomicSequence(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure.assemblyName));
     };
 }
 
@@ -54,7 +54,7 @@ export function water(): StructureQuery {
             if (P.entity.type(l) !== 'water') continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure.assemblyName));
     };
 }
 
@@ -84,7 +84,7 @@ export function atomicHet(): StructureQuery {
 
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure.assemblyName));
     };
 }
 
@@ -97,6 +97,6 @@ export function spheres(): StructureQuery {
             if (unit.kind !== Unit.Kind.Spheres) continue;
             units.push(unit);
         }
-        return StructureSelection.Singletons(inputStructure, new Structure(units));
+        return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure.assemblyName));
     };
 }

+ 1 - 1
src/mol-model/structure/query/selection.ts

@@ -109,7 +109,7 @@ namespace StructureSelection {
                 const { elements } = unit;
                 for (let i = 0, _i = elements.length; i < _i; i++) {
                     // TODO: optimize this somehow???
-                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))]);
+                    const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))], sel.structure.assemblyName);
                     fn(s, idx++);
                 }
             }

+ 2 - 2
src/mol-model/structure/query/utils/structure-set.ts

@@ -80,7 +80,7 @@ export function structureIntersect(sA: Structure, sB: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, sA.assemblyName === sB.assemblyName ? sA.assemblyName : '');
 }
 
 export function structureSubtract(a: Structure, b: Structure): Structure {
@@ -100,5 +100,5 @@ export function structureSubtract(a: Structure, b: Structure): Structure {
         }
     }
 
-    return Structure.create(units);
+    return Structure.create(units, a.assemblyName === b.assemblyName ? a.assemblyName : '');
 }

+ 16 - 8
src/mol-model/structure/structure/structure.ts

@@ -2,6 +2,7 @@
  * 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 { IntMap, SortedArray, Iterator, Segmentation } from 'mol-data/int'
@@ -48,7 +49,8 @@ class Structure {
         transformHash: number,
         elementCount: number,
         polymerResidueCount: number,
-    } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 };
+        assemblyName: string
+    } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0, assemblyName: '' };
 
     subsetBuilder(isSorted: boolean) {
         return new StructureSubsetBuilder(this, isSorted);
@@ -64,6 +66,11 @@ class Structure {
         return this._props.polymerResidueCount;
     }
 
+    /** Name of the assembly given by `_pdbx_struct_assembly.id` when applicable */
+    get assemblyName() {
+        return this._props.assemblyName;
+    }
+
     /** Coarse structure, defined as Containing less than twice as many elements as polymer residues */
     get isCoarse() {
         const ec = this.elementCount
@@ -169,7 +176,7 @@ class Structure {
         return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element);
     }
 
-    constructor(units: ArrayLike<Unit>) {
+    constructor(units: ArrayLike<Unit>, assemblyName: string) {
         const map = IntMap.Mutable<Unit>();
         let elementCount = 0;
         let polymerResidueCount = 0;
@@ -188,6 +195,7 @@ class Structure {
         this.units = units as ReadonlyArray<Unit>;
         this._props.elementCount = elementCount;
         this._props.polymerResidueCount = polymerResidueCount;
+        this._props.assemblyName = assemblyName
     }
 }
 
@@ -278,7 +286,7 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID,
 }
 
 namespace Structure {
-    export const Empty = new Structure([]);
+    export const Empty = new Structure([], '');
 
     /** Represents a single structure */
     export interface Loci {
@@ -297,7 +305,7 @@ namespace Structure {
         return a.structure === b.structure
     }
 
-    export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); }
+    export function create(units: ReadonlyArray<Unit>, assemblyName: string): Structure { return new Structure(units, assemblyName); }
 
     /**
      * Construct a Structure from a model.
@@ -338,7 +346,7 @@ namespace Structure {
             }
         }
 
-        return builder.getStructure();
+        return builder.getStructure('deposited');
     }
 
     function isWaterChain(model: Model, chainIndex: ChainIndex, indices: SortedArray) {
@@ -380,7 +388,7 @@ namespace Structure {
             units.push(u.applyOperator(u.id, op));
         }
 
-        return new Structure(units);
+        return new Structure(units, s.assemblyName);
     }
 
     export class StructureBuilder {
@@ -399,8 +407,8 @@ namespace Structure {
             return newUnit;
         }
 
-        getStructure(): Structure {
-            return create(this.units);
+        getStructure(assemblyName: string): Structure {
+            return create(this.units, assemblyName);
         }
 
         get isEmpty() {

+ 3 - 3
src/mol-model/structure/structure/symmetry.ts

@@ -42,7 +42,7 @@ namespace StructureSymmetry {
                 }
             }
 
-            return assembler.getStructure();
+            return assembler.getStructure(asmName);
         });
     }
 
@@ -118,7 +118,7 @@ function assembleOperators(structure: Structure, operators: ReadonlyArray<Symmet
             assembler.addWithOperator(unit, oper);
         }
     }
-    return assembler.getStructure();
+    return assembler.getStructure(structure.assemblyName);
 }
 
 async function _buildNCS(ctx: RuntimeContext, structure: Structure) {
@@ -173,7 +173,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius
     }
 
 
-    return assembler.getStructure();
+    return assembler.getStructure(structure.assemblyName);
 }
 
 export default StructureSymmetry;

+ 5 - 9
src/mol-model/structure/structure/util/nucleotide.ts

@@ -6,15 +6,13 @@
 
 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';
+import { isNucleic } from 'mol-model/structure/model/types';
 
 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 { chainAtomSegments, residueAtomSegments } = model.atomicHierarchy
+    const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
     const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
     while (chainIt.hasNext) {
@@ -22,11 +20,9 @@ export function getNucleotideElements(unit: Unit.Atomic) {
 
         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')
+            if (isNucleic(moleculeType[index])) {
+                const elementIndex = traceElementIndex[index]
                 indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
             }
         }

+ 5 - 4
src/mol-model/structure/structure/util/polymer.ts

@@ -7,12 +7,12 @@
 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 { traceElementIndex } = model.atomicHierarchy.derived.residue
     const polymerIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.polymerRanges, elements)
     const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
     while (polymerIt.hasNext) {
@@ -22,7 +22,7 @@ export function getAtomicPolymerElements(unit: Unit.Atomic) {
             const residueSegment = residueIt.move()
             const { start, end, index } = residueSegment
             if (OrderedSet.areIntersecting(Interval.ofRange(elements[start], elements[end - 1]), elements)) {
-                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                const elementIndex = traceElementIndex[index]
                 indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
             }
         }
@@ -47,13 +47,14 @@ export function getAtomicGapElements(unit: Unit.Atomic) {
     const indices: ElementIndex[] = []
     const { elements, model, residueIndex } = unit
     const { residueAtomSegments } = unit.model.atomicHierarchy
+    const { traceElementIndex } = model.atomicHierarchy.derived.residue
     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')
+        const elementIndexStart = traceElementIndex[indexStart]
+        const elementIndexEnd = traceElementIndex[indexEnd]
         indices.push(elementIndexStart === -1 ? residueAtomSegments.offsets[indexStart] : elementIndexStart)
         indices.push(elementIndexEnd === -1 ? residueAtomSegments.offsets[indexEnd] : elementIndexEnd)
 

+ 1 - 1
src/mol-model/structure/structure/util/subset-builder.ts

@@ -90,7 +90,7 @@ export class StructureSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent.assemblyName);
     }
 
     getStructure() {

+ 1 - 1
src/mol-model/structure/structure/util/unique-subset-builder.ts

@@ -85,7 +85,7 @@ export class StructureUniqueSubsetBuilder {
             newUnits[newUnits.length] = child;
         }
 
-        return Structure.create(newUnits);
+        return Structure.create(newUnits, this.parent.assemblyName);
     }
 
     get isEmpty() {

+ 0 - 5
src/mol-model/structure/util.ts

@@ -49,11 +49,6 @@ export function getAtomIdForAtomRole(moleculeType: MoleculeType, atomRole: AtomR
     return ''
 }
 
-export function getElementIndexForAtomRole(model: Model, rI: ResidueIndex, atomRole: AtomRole) {
-    const atomId = getAtomIdForAtomRole(getAtomicMoleculeType(model, rI), atomRole)
-    return model.atomicHierarchy.index.findAtomOnResidue(rI, atomId)
-}
-
 export function residueLabel(model: Model, rI: number) {
     const { residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy
     const { label_comp_id, label_seq_id } = residues

+ 3 - 75
src/mol-plugin/behavior/dynamic/custom-props.ts

@@ -2,80 +2,8 @@
  * 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 { OrderedSet } from 'mol-data/int';
-import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
-import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report';
-import { Loci } from 'mol-model/loci';
-import { StructureElement } from 'mol-model/structure';
-import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { PluginBehavior } from '../behavior';
-
-export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
-    name: 'pdbe-structure-quality-report-prop',
-    display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
-    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
-        private attach = StructureQualityReport.createAttachTask(
-            m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
-            this.ctx.fetch
-        );
-
-        private provider: CustomPropertyRegistry.Provider = {
-            option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
-            descriptor: StructureQualityReport.Descriptor,
-            defaultSelected: this.params.autoAttach,
-            attachableTo: () => true,
-            attach: this.attach
-        }
-
-        register(): void {
-            this.ctx.customModelProperties.register(this.provider);
-            this.ctx.lociLabels.addProvider(labelPDBeValidation);
-
-            // TODO: support filtering of themes based on the input structure
-            // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
-            // TODO: add remove functionality
-            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
-                label: 'PDBe Structure Quality Report',
-                factory: StructureQualityReportColorTheme,
-                getParams: () => ({})
-            })
-        }
-
-        update(p: { autoAttach: boolean }) {
-            let updated = this.params.autoAttach !== p.autoAttach
-            this.params.autoAttach = p.autoAttach;
-            this.provider.defaultSelected = p.autoAttach;
-            return updated;
-        }
-
-        unregister() {
-            this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
-            this.ctx.lociLabels.removeProvider(labelPDBeValidation);
-
-            // TODO: add remove functionality to registry
-            // this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
-        }
-    },
-    params: () => ({
-        autoAttach: PD.Boolean(false)
-    })
-});
-
-function labelPDBeValidation(loci: Loci): string | undefined {
-    switch (loci.kind) {
-        case 'element-loci':
-            const e = loci.elements[0];
-            const u = e.unit;
-            if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
-
-            const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
-            const issues = StructureQualityReport.getIssues(se);
-            if (issues.length === 0) return 'PDBe Validation: No Issues';
-            return `PDBe Validation: ${issues.join(', ')}`;
-
-        default: return void 0;
-    }
-}
+export { PDBeStructureQualityReport } from './custom-props/pdbe/structure-quality-report'
+export { RCSBAssemblySymmetry } from './custom-props/rcsb/assembly-symmetry'

+ 79 - 0
src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts

@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { OrderedSet } from 'mol-data/int';
+import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
+import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report';
+import { Loci } from 'mol-model/loci';
+import { StructureElement } from 'mol-model/structure';
+import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { PluginBehavior } from '../../../behavior';
+
+export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'pdbe-structure-quality-report-prop',
+    display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        private attach = StructureQualityReport.createAttachTask(
+            m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`,
+            this.ctx.fetch
+        );
+
+        private provider: CustomPropertyRegistry.Provider = {
+            option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'],
+            descriptor: StructureQualityReport.Descriptor,
+            defaultSelected: this.params.autoAttach,
+            attachableTo: () => true,
+            attach: this.attach
+        }
+
+        register(): void {
+            this.ctx.customModelProperties.register(this.provider);
+            this.ctx.lociLabels.addProvider(labelPDBeValidation);
+
+            // TODO: support filtering of themes based on the input structure
+            // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor)
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', {
+                label: 'PDBe Structure Quality Report',
+                factory: StructureQualityReportColorTheme,
+                getParams: () => ({}),
+                defaultValues: {}
+            })
+        }
+
+        update(p: { autoAttach: boolean }) {
+            let updated = this.params.autoAttach !== p.autoAttach
+            this.params.autoAttach = p.autoAttach;
+            this.provider.defaultSelected = p.autoAttach;
+            return updated;
+        }
+
+        unregister() {
+            this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name);
+            this.ctx.lociLabels.removeProvider(labelPDBeValidation);
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report')
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false)
+    })
+});
+
+function labelPDBeValidation(loci: Loci): string | undefined {
+    switch (loci.kind) {
+        case 'element-loci':
+            const e = loci.elements[0];
+            const u = e.unit;
+            if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0;
+
+            const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]);
+            const issues = StructureQualityReport.getIssues(se);
+            if (issues.length === 0) return 'PDBe Validation: No Issues';
+            return `PDBe Validation: ${issues.join(', ')}`;
+
+        default: return void 0;
+    }
+}

+ 50 - 0
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from 'mol-plugin/behavior';
+import { ParamDefinition as PD } from 'mol-util/param-definition'
+import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry';
+import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry';
+import { AssemblySymmetryClusterColorThemeProvider } from 'mol-model-props/rcsb/themes/assembly-symmetry';
+
+export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({
+    name: 'rcsb-assembly-symmetry-prop',
+    display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' },
+    ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
+        private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch);
+
+        private provider: CustomPropertyRegistry.Provider = {
+            option: [AssemblySymmetry.Descriptor.name, 'RCSB Assembly Symmetry'],
+            descriptor: AssemblySymmetry.Descriptor,
+            defaultSelected: this.params.autoAttach,
+            attachableTo: () => true,
+            attach: this.attach
+        }
+
+        register(): void {
+            this.ctx.customModelProperties.register(this.provider);
+
+            // TODO: support filtering of themes based on the input structure
+            // in this case, it would check structure.models[0].customProperties.has(AssemblySymmetry.Descriptor)
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('rcsb-assembly-symmetry-cluster', AssemblySymmetryClusterColorThemeProvider)
+        }
+
+        update(p: { autoAttach: boolean }) {
+            let updated = this.params.autoAttach !== p.autoAttach
+            this.params.autoAttach = p.autoAttach;
+            this.provider.defaultSelected = p.autoAttach;
+            return updated;
+        }
+
+        unregister() {
+            this.ctx.customModelProperties.unregister(AssemblySymmetry.Descriptor.name);
+            this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('rcsb-assembly-symmetry-cluster')
+        }
+    },
+    params: () => ({
+        autoAttach: PD.Boolean(false)
+    })
+});

+ 2 - 2
src/mol-plugin/context.ts

@@ -103,8 +103,8 @@ export class PluginContext {
      * This should be used in all transform related request so that it could be "spoofed" to allow
      * "static" access to resources.
      */
-    fetch(url: string, type: 'string' | 'binary' = 'string'): Task<string | Uint8Array> {
-        return ajaxGet({ url, type });
+    fetch(url: string, type: 'string' | 'binary' = 'string', body?: string): Task<string | Uint8Array> {
+        return ajaxGet({ url, type, body });
         // const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' });
         // return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
     }

+ 2 - 1
src/mol-plugin/index.ts

@@ -36,7 +36,8 @@ const DefaultSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
-        PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true })
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
     ]
 }
 

+ 64 - 0
src/mol-plugin/skin/base/components/line-graph.scss

@@ -0,0 +1,64 @@
+.msp-canvas {
+    width: 100%;
+    height: 100%;
+    background-color: #f3f2ee;
+    
+    text {
+        -webkit-touch-callout: none; /* iOS Safari */
+        -webkit-user-select: none; /* Safari */
+        -khtml-user-select: none; /* Konqueror HTML */
+        -moz-user-select: none; /* Firefox */
+        -ms-user-select: none; /* Internet Explorer/Edge */
+        user-select: none; /* Non-prefixed version, currently
+                              supported by Chrome and Opera */
+    }
+
+    circle {
+        stroke: black;
+        stroke-width: 10;
+        stroke-opacity: .3;
+
+        &:hover {
+            fill: #ae5d04;
+            stroke: black;
+            stroke-width: 10px;
+        }
+    }
+
+    .info {
+        fill: white;
+        stroke: black;
+        stroke-width: 3;
+    }
+
+    .show {
+        visibility: visible;
+    }
+    .hide {
+        visibility: hidden;
+    }
+
+    .delete-button {
+        rect {
+            fill: #ED4337;
+            stroke: black;
+        }
+        
+        text {
+            stroke: white;
+            fill: white;
+        }
+
+        &:hover {
+            stroke: black;
+            stroke-width: 3;
+            fill: #ff6961;
+        }
+    }
+
+    .infoCircle {
+        &:hover {
+            fill: #4c66b2;
+        }
+    }
+}

+ 2 - 1
src/mol-plugin/skin/base/ui.scss

@@ -40,4 +40,5 @@
 @import 'components/transformer';
 @import 'components/toast';
 @import 'components/help';
-@import 'components/temp';
+@import 'components/temp';
+@import 'components/line-graph.scss';

+ 10 - 12
src/mol-plugin/state/transforms/representation.ts

@@ -14,33 +14,31 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { createTheme } from 'mol-theme/theme';
 import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry';
 import { Structure } from 'mol-model/structure';
-import { UnitsMeshParams } from 'mol-repr/structure/units-visual';
+import { StructureParams } from 'mol-repr/structure/representation';
 
 export namespace StructureRepresentation3DHelpers {
-    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, meshParams?: Partial<PD.Values<UnitsMeshParams>>): Transformer.Params<StructureRepresentation3D> {
+    export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
 
         const themeDataCtx = { structure };
         const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).getParams(themeDataCtx);
         const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).getParams(themeDataCtx)
+        const structureDefaultParams = PD.getDefaultValues(type.getParams(ctx.structureRepresentation.themeCtx, structure))
         return ({
-            type: { name, params: meshParams ? { ...type.defaultValues, ...meshParams } : type.defaultValues },
+            type: { name, params: structureParams ? { ...structureDefaultParams, ...structureParams } : structureDefaultParams },
             colorTheme: { name: type.defaultColorTheme, params: PD.getDefaultValues(colorParams) },
             sizeTheme: { name: type.defaultSizeTheme, params: PD.getDefaultValues(sizeParams) }
         })
     }
 
-    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, meshParams?: Partial<PD.Values<UnitsMeshParams>>): Transformer.Params<StructureRepresentation3D> {
+    export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> {
         const type = ctx.structureRepresentation.registry.get(name);
-
-        // TODO: there should be "static default properties" for the themes.
-        const themeDataCtx = { };
-        const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).getParams(themeDataCtx);
-        const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).getParams(themeDataCtx)
+        const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues;
+        const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues
         return ({
-            type: { name, params: meshParams ? { ...type.defaultValues, ...meshParams } : type.defaultValues },
-            colorTheme: { name: type.defaultColorTheme, params: PD.getDefaultValues(colorParams) },
-            sizeTheme: { name: type.defaultSizeTheme, params: PD.getDefaultValues(sizeParams) }
+            type: { name, params: structureParams ? { ...type.defaultValues, ...structureParams } : type.defaultValues },
+            colorTheme: { name: type.defaultColorTheme, params: colorParams },
+            sizeTheme: { name: type.defaultSizeTheme, params: sizeParams }
         })
     }
 }

+ 354 - 0
src/mol-plugin/ui/controls/line-graph/line-graph-component.tsx

@@ -0,0 +1,354 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Paul Luna <paulluna0215@gmail.com>
+ */
+import PointComponent from './point-component';
+
+import * as React from 'react';
+import { Vec2 } from 'mol-math/linear-algebra';
+
+interface LineGraphComponentState {
+    points: Vec2[],
+    selected?: number,
+    copyPoint: any,
+    updatedX: number, 
+    updatedY: number,
+}
+
+export default class LineGraphComponent extends React.Component<any, LineGraphComponentState> {
+    private myRef:any;
+    private height: number;
+    private width: number;
+    private padding: number;
+    
+    constructor(props: any) {
+        super(props);
+        this.myRef = React.createRef();
+        this.state = {
+            points:[
+                Vec2.create(0, 0),
+                Vec2.create(1, 0)
+            ],
+            selected: undefined,
+            copyPoint: undefined,
+            updatedX: 0,
+            updatedY: 0,
+        };
+        this.height = 400;
+        this.width = 600;
+        this.padding = 70;
+        
+        for (const point of this.props.data){
+            this.state.points.push(point);
+        }
+        
+        this.state.points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+
+        this.handleDrag = this.handleDrag.bind(this);
+        this.handleDoubleClick = this.handleDoubleClick.bind(this);
+        this.refCallBack = this.refCallBack.bind(this);
+        this.handlePointUpdate = this.handlePointUpdate.bind(this);
+        this.change = this.change.bind(this);
+    }
+
+    public render() {
+        const points = this.renderPoints();
+        const ghostPoint = this.state.copyPoint;
+        return ([
+            <div key="LineGraph">                
+                <svg
+                    className="msp-canvas"
+                    ref={this.refCallBack} 
+                    viewBox={`0 0 ${this.width+this.padding} ${this.height+this.padding}`}
+                    onMouseMove={this.handleDrag} 
+                    onMouseUp={this.handlePointUpdate}
+                    onDoubleClick={this.handleDoubleClick}>  
+            
+                    <g stroke="black" fill="black">
+                        <Poly 
+                            data={this.state.points} 
+                            k={0.5}
+                            height={this.height}
+                            width={this.width}
+                            padding={this.padding}/>
+                        {points}
+                        {ghostPoint}
+                    </g>
+
+                     <defs>
+                        <linearGradient id="Gradient">
+                            <stop offset="0%" stopColor="#d30000"/>
+                            <stop offset="30%" stopColor="#ffff05"/>
+                            <stop offset="50%" stopColor="#05ff05"/>
+                            <stop offset="70%" stopColor="#05ffff"/>
+                            <stop offset="100%" stopColor="#041ae0"/>
+                        </linearGradient>
+                    </defs>
+                    
+                </svg>
+            </div>,
+            <div key="modal" id="modal-root" />
+        ]);
+    }
+
+    private change(points: Vec2[]){
+        let copyPoints = points.slice();
+        copyPoints.shift();
+        copyPoints.pop();
+        this.props.onChange(copyPoints);    
+    }
+
+    private handleMouseDown = (id:number) => (event: any) => {
+        if(id === 0 || id === this.state.points.length-1){
+            return;
+        } 
+        const copyPoint: Vec2 = this.normalizePoint(Vec2.create(this.state.points[id][0], this.state.points[id][1]));
+        this.setState({
+            selected: id,
+            copyPoint: "ready",
+            updatedX: copyPoint[0],
+            updatedY: copyPoint[1],
+        });
+
+        event.preventDefault();
+    }
+
+    private handleDrag(event: any) {
+        if(this.state.copyPoint === undefined){
+            return
+        }
+        const pt = this.myRef.createSVGPoint();
+        let updatedCopyPoint;
+        const padding = this.padding/2;
+        pt.x = event.clientX;
+        pt.y = event.clientY;
+        const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
+
+        if( svgP.x < (padding) || 
+            svgP.x > (this.width+(padding)) || 
+            svgP.y > (this.height+(padding)) || 
+            svgP.y < (padding)) {
+            return;
+        }
+        updatedCopyPoint = Vec2.create(svgP.x, svgP.y);
+        this.setState({
+            updatedX: updatedCopyPoint[0],
+            updatedY: updatedCopyPoint[1],
+        });
+        const unNormalizePoint = this.unNormalizePoint(updatedCopyPoint);
+        this.setState({
+            copyPoint: <PointComponent 
+                            selected={false}
+                            key="copy" 
+                            x={updatedCopyPoint[0]} 
+                            y={updatedCopyPoint[1]} 
+                            nX={unNormalizePoint[0]} 
+                            nY={unNormalizePoint[1]}
+                            delete={this.deletePoint}
+                            onmouseover={this.props.onHover}/>
+        });
+        this.props.onDrag(unNormalizePoint);
+        event.preventDefault()
+    }
+
+    private handlePointUpdate(event: any) {
+        const selected = this.state.selected;
+        if(selected === undefined || selected === 0 || selected === this.state.points.length-1) {
+            this.setState({
+                selected: undefined,
+                copyPoint: undefined,
+            });
+            return
+        }
+        const updatedPoint = this.unNormalizePoint(Vec2.create(this.state.updatedX, this.state.updatedY));
+        const points = this.state.points.filter((_,i) => i !== this.state.selected);
+        points.push(updatedPoint);;
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({
+            points,
+            selected: undefined,
+            copyPoint: undefined,
+        });
+        this.change(points);
+        event.preventDefault();
+    }
+
+    private handleDoubleClick(event: any) {
+        let newPoint;
+        const pt = this.myRef.createSVGPoint();
+        pt.x = event.clientX;
+        pt.y = event.clientY;
+        const svgP = pt.matrixTransform(this.myRef.getScreenCTM().inverse());
+        const points = this.state.points;
+        const padding = this.padding/2; 
+
+        if( svgP.x < (padding) || 
+            svgP.x > (this.width+(padding)) || 
+            svgP.y > (this.height+(padding)) || 
+            svgP.y < (this.padding/2)) {
+            return;
+        }
+        newPoint = this.unNormalizePoint(Vec2.create(svgP.x, svgP.y));
+        points.push(newPoint);
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({points})
+        this.change(points);
+        event.preventDefault();
+    }
+    private deletePoint = (i:number) => (event: any) => {
+    if(i===0 || i===this.state.points.length-1){ return};
+        const points = this.state.points.filter((_,j) => j !== i);
+        points.sort((a, b) => { 
+            if(a[0] === b[0]){
+                if(a[0] === 0){
+                    return a[1]-b[1];
+                }
+                if(a[1] === 1){
+                    return b[1]-a[1];
+                }
+                return a[1]-b[1];
+            }
+            return a[0] - b[0];
+        });
+        this.setState({points});
+        this.change(points);
+        event.stopPropagation();
+    }
+
+    private normalizePoint(point: Vec2) {
+        const min = this.padding/2;
+        const maxX = this.width+min;
+        const maxY = this.height+min; 
+        const normalizedX = (point[0]*(maxX-min))+min; 
+        const normalizedY = (point[1]*(maxY-min))+min;
+        const reverseY = (this.height+this.padding)-normalizedY;
+        const newPoint = Vec2.create(normalizedX, reverseY);
+        return newPoint;
+    }
+
+    private unNormalizePoint(point: Vec2) {
+        const min = this.padding/2;
+        const maxX = this.width+min; 
+        const maxY = this.height+min;
+        const unNormalizedX = (point[0]-min)/(maxX-min);
+
+        // we have to take into account that we reversed y when we first normalized it.
+        const unNormalizedY = ((this.height+this.padding)-point[1]-min)/(maxY-min); 
+
+        return Vec2.create(unNormalizedX, unNormalizedY);
+    }
+
+    private refCallBack(element: any) {
+        if(element){
+            this.myRef = element;
+        }
+    }
+
+    private renderPoints() {
+        const points: any[] = [];
+        let point: Vec2;
+        for (let i = 0; i < this.state.points.length; i++){
+            if(i != 0 && i != this.state.points.length-1){
+                point = this.normalizePoint(this.state.points[i]);
+                points.push(<PointComponent
+                        key={i}
+                        id={i}
+                        x={point[0]} 
+                        y={point[1]}
+                        nX={this.state.points[i][0]}
+                        nY={this.state.points[i][1]}
+                        selected={false}
+                        delete={this.deletePoint}
+                        onmouseover={this.props.onHover}
+                        onMouseDown={this.handleMouseDown(i)}
+                    />);
+            }
+        }
+        return points;
+    }
+}
+
+function Poly(props: any) {
+
+    const points: Vec2[] = [];
+    let min:number;
+    let maxX:number;
+    let maxY: number;
+    let normalizedX: number;
+    let normalizedY: number;
+    let reverseY: number;
+    
+    for(const point of props.data){
+        min = parseInt(props.padding, 10)/2;
+        maxX = parseInt(props.width, 10)+min;
+        maxY = parseInt(props.height, 10)+min; 
+        normalizedX = (point[0]*(maxX-min))+min; 
+        normalizedY = (point[1]*(maxY-min))+min;
+        reverseY = (props.height+props.padding)-normalizedY;
+        points.push(Vec2.create(normalizedX, reverseY));
+    }
+
+    if (props.k == null) {props.k = 0.3};
+    const data = points;
+    const size = data.length;
+    const last = size - 2;
+    let path = "M" + [data[0][0], data[0][1]];
+
+    for (let i=0; i<size-1;i++){
+        const x0 = i ? data[i-1][0] : data[0][0];
+        const y0 = i ? data[i-1][1] : data[0][1];
+
+        const x1 = data[i][0];
+        const y1 = data[i][1];
+
+        const x2 = data[i+1][0];
+        const y2 = data[i+1][1];
+
+        const x3 = i !== last ? data[i+2][0] : x2;
+        const y3 = i !== last ? data[i+2][1] : y2; 
+
+        const cp1x = x1 + (x2 - x0)/6 * props.k;
+        const cp1y = y1 + (y2 -y0)/6 * props.k;
+
+        const cp2x = x2 - (x3 -x1)/6 * props.k;
+        const cp2y = y2 - (y3 - y1)/6 * props.k;
+
+        path += "C" + [cp1x, cp1y, cp2x, cp2y, x2, y2];
+    }
+
+    return <path d={path} strokeWidth="5" stroke="#cec9ba" fill="none"/>
+}

+ 47 - 0
src/mol-plugin/ui/controls/line-graph/point-component.tsx

@@ -0,0 +1,47 @@
+
+import * as React from 'react';
+
+import { Vec2 } from 'mol-math/linear-algebra';
+
+export default class PointComponent extends React.Component<any, {show: boolean}> {
+    constructor(props: any){
+        super(props);
+        this.state = {show: false}
+        
+        this.handleHover = this.handleHover.bind(this);
+        this.handleHoverOff = this.handleHoverOff.bind(this);
+        this.deletePoint = this.deletePoint.bind(this);
+    }
+
+    private handleHover() {
+        this.setState({show: true});
+        const point = Vec2.create(this.props.nX, this.props.nY);
+        this.props.onmouseover(point);
+    }
+
+    private handleHoverOff(){
+        this.setState({show: false});
+        this.props.onmouseover(undefined);
+    }
+
+    private deletePoint() {
+        this.props.delete(this.props.id);   
+    }
+
+    public render() {
+        return([
+            <circle 
+                r="10"
+                key={`${this.props.id}circle`}
+                id={`${this.props.id}`}
+                cx={this.props.x} 
+                cy={this.props.y} 
+                onDoubleClick={this.props.delete(this.props.id)}
+                onMouseEnter={this.handleHover} 
+                onMouseLeave={this.handleHoverOff}
+                onMouseDown={this.props.onMouseDown}
+                fill="black"
+            />
+        ]);
+    }
+}

+ 65 - 3
src/mol-plugin/ui/controls/parameters.tsx

@@ -6,12 +6,17 @@
  */
 
 import * as React from 'react'
+
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { camelCaseToWords } from 'mol-util/string';
 import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
 import { Color } from 'mol-util/color';
+import { Vec2 } from 'mol-math/linear-algebra';
+import LineGraphComponent from './line-graph/line-graph-component';
+
 import { Slider, Slider2 } from './slider';
 
+
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
@@ -53,7 +58,7 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         ? BoundedIntervalControl : IntervalControl;
         case 'group': return GroupControl;
         case 'mapped': return MappedControl;
-        case 'line-graph': return void 0;
+        case 'line-graph': return LineGraphControl;
     }
     console.warn(`${(param as any).type} has no associated UI component.`);
     return void 0;
@@ -93,6 +98,57 @@ export class BoolControl extends SimpleParam<PD.Boolean> {
     }
 }
 
+export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGraph>, { isExpanded: boolean, isOverPoint: boolean, message: string }> {
+    state = {
+        isExpanded: false,
+        isOverPoint: false,
+        message: `${this.props.param.defaultValue.length} points`,
+    }
+
+    onHover = (point?: Vec2) => {
+        this.setState({isOverPoint: !this.state.isOverPoint});
+        if (point) {
+            this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+            return;
+        }
+        this.setState({message: `${this.props.value.length} points`});
+    }
+
+    onDrag = (point: Vec2) => {
+        this.setState({message: `(${point[0].toFixed(2)}, ${point[1].toFixed(2)})`});
+    }
+
+    onChange = (value: PD.LineGraph['defaultValue'] ) => {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value: value});
+    }
+
+    toggleExpanded = (e: React.MouseEvent<HTMLButtonElement>) => {
+        this.setState({ isExpanded: !this.state.isExpanded });
+        e.currentTarget.blur();
+    }
+
+    render() {
+        const label = this.props.param.label || camelCaseToWords(this.props.name);
+        return <>
+            <div className='msp-control-row'>
+                <span>{label}</span>
+                <div>
+                    <button onClick={this.toggleExpanded}>
+                        {`${this.state.message}`}
+                    </button>
+                </div>
+            </div>
+            <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
+                <LineGraphComponent
+                    data={this.props.param.defaultValue}
+                    onChange={this.onChange}
+                    onHover={this.onHover}
+                    onDrag={this.onDrag}/>
+            </div>
+        </>;
+    }
+}
+
 export class NumberInputControl extends SimpleParam<PD.Numeric> {
     onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.update(+e.target.value); }
     renderControl() {
@@ -137,8 +193,14 @@ export class TextControl extends SimpleParam<PD.Text> {
     }
 }
 
-export class SelectControl extends SimpleParam<PD.Select<any>> {
-    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(e.target.value); }
+export class SelectControl extends SimpleParam<PD.Select<string | number>> {
+    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        if (typeof this.props.param.defaultValue === 'number') {
+            this.update(parseInt(e.target.value, 10));
+        } else {
+            this.update(e.target.value);
+        }
+    }
     renderControl() {
         return <select value={this.props.value || ''} onChange={this.onChange} disabled={this.props.isDisabled}>
             {this.props.param.options.map(([value, label]) => <option key={value} value={value}>{label}</option>)}

+ 1 - 1
src/mol-plugin/ui/state.tsx

@@ -9,8 +9,8 @@ import * as React from 'react';
 import { PluginComponent } from './base';
 import { shallowEqual } from 'mol-util';
 import { List } from 'immutable';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ParameterControls } from './controls/parameters';
+import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 
 export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> {

+ 5 - 0
src/mol-repr/representation.ts

@@ -70,6 +70,11 @@ export class RepresentationRegistry<D> {
         this._map.set(name, provider)
     }
 
+    remove(name: string) {
+        this._list.splice(this._list.findIndex(e => e.name === name))
+        this._map.delete(name)
+    }
+
     get<P extends PD.Params>(name: string): RepresentationProvider<D, P> {
         return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P>
     }

+ 1 - 1
src/mol-repr/structure/complex-visual.ts

@@ -155,7 +155,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
 
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
-        get renderObject () { return renderObject },
+        get renderObject () { return locationIt && locationIt.count ? renderObject : undefined },
         createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, structure?: Structure) {
             prepareUpdate(theme, props, structure || currentStructure)
             if (updateState.createGeometry) {

+ 12 - 2
src/mol-repr/structure/representation/cartoon.ts

@@ -12,7 +12,7 @@ import { UnitsRepresentation } from '../units-representation';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
 import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { PolymerDirectionVisual, PolymerDirectionParams } from '../visual/polymer-direction-wedge';
-import { Structure } from 'mol-model/structure';
+import { Structure, Unit } from 'mol-model/structure';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 
 const CartoonVisuals = {
@@ -34,7 +34,17 @@ export const CartoonParams = {
 }
 export type CartoonParams = typeof CartoonParams
 export function getCartoonParams(ctx: ThemeRegistryContext, structure: Structure) {
-    return PD.clone(CartoonParams)
+    const params = PD.clone(CartoonParams)
+    let hasNucleotides = false
+    let hasGaps = false
+    structure.units.forEach(u => {
+        if (!hasNucleotides && Unit.isAtomic(u) && u.nucleotideElements.length) hasNucleotides = true
+        if (!hasGaps && u.gapElements.length) hasGaps = true
+    })
+    params.visuals.defaultValue = ['polymer-trace']
+    if (hasNucleotides) params.visuals.defaultValue.push('nucleotide-block')
+    if (hasGaps) params.visuals.defaultValue.push('polymer-gap')
+    return params
 }
 
 export type CartoonRepresentation = StructureRepresentation<CartoonParams>

+ 1 - 1
src/mol-repr/structure/representation/distance-restraint.ts

@@ -32,7 +32,7 @@ export function DistanceRestraintRepresentation(ctx: RepresentationContext, getP
 }
 
 export const DistanceRestraintRepresentationProvider: StructureRepresentationProvider<typeof DistanceRestraintParams> = {
-    label: 'DistanceRestraint',
+    label: 'Distance Restraint',
     description: 'Displays cross-link distance restraints.',
     factory: DistanceRestraintRepresentation,
     getParams: getDistanceRestraintParams,

+ 1 - 1
src/mol-repr/structure/units-visual.ts

@@ -198,7 +198,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
 
     return {
         get groupCount() { return locationIt ? locationIt.count : 0 },
-        get renderObject () { return renderObject },
+        get renderObject () { return locationIt && locationIt.count ? renderObject : undefined },
         createOrUpdate(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}, structureGroup?: StructureGroup) {
             prepareUpdate(theme, props, structureGroup || currentStructureGroup)
             if (updateState.createGeometry) {

+ 21 - 21
src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts

@@ -45,7 +45,7 @@ const pentagonalPrism = PentagonalPrism()
 const hexagonalPrism = HexagonalPrism()
 
 function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<CarbohydrateSymbolParams>, mesh?: Mesh) {
-    const builder = MeshBuilder.create(256, 128, mesh)
+    const builderState = MeshBuilder.createState(256, 128, mesh)
 
     const { detail, sizeFactor } = props
 
@@ -68,77 +68,77 @@ function createCarbohydrateSymbolMesh(ctx: VisualContext, structure: Structure,
         Mat4.targetTo(t, center, pd, normal)
         Mat4.setTranslation(t, center)
 
-        builder.setGroup(i * 2)
+        builderState.currentGroup = i * 2
 
         switch (shapeType) {
             case SaccharideShapes.FilledSphere:
-                addSphere(builder, center, radius, detail)
+                addSphere(builderState, center, radius, detail)
                 break;
             case SaccharideShapes.FilledCube:
                 Mat4.scaleUniformly(t, t, side)
-                builder.add(t, box)
+                MeshBuilder.addPrimitive(builderState, t, box)
                 break;
             case SaccharideShapes.CrossedCube:
                 Mat4.scaleUniformly(t, t, side)
-                builder.add(t, perforatedBox)
+                MeshBuilder.addPrimitive(builderState, t, perforatedBox)
                 Mat4.mul(t, t, Mat4.rotZ90X180)
-                builder.setGroup(i * 2 + 1)
-                builder.add(t, perforatedBox)
+                builderState.currentGroup = i * 2 + 1
+                MeshBuilder.addPrimitive(builderState, t, perforatedBox)
                 break;
             case SaccharideShapes.FilledCone:
                 Mat4.scaleUniformly(t, t, side * 1.2)
-                builder.add(t, octagonalPyramid)
+                MeshBuilder.addPrimitive(builderState, t, octagonalPyramid)
                 break
             case SaccharideShapes.DevidedCone:
                 Mat4.scaleUniformly(t, t, side * 1.2)
-                builder.add(t, perforatedOctagonalPyramid)
+                MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
                 Mat4.mul(t, t, Mat4.rotZ90)
-                builder.setGroup(i * 2 + 1)
-                builder.add(t, perforatedOctagonalPyramid)
+                builderState.currentGroup = i * 2 + 1
+                MeshBuilder.addPrimitive(builderState, t, perforatedOctagonalPyramid)
                 break
             case SaccharideShapes.FlatBox:
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
-                builder.add(t, box)
+                MeshBuilder.addPrimitive(builderState, t, box)
                 break
             case SaccharideShapes.FilledStar:
                 Mat4.scaleUniformly(t, t, side)
                 Mat4.mul(t, t, Mat4.rotZY90)
-                builder.add(t, star)
+                MeshBuilder.addPrimitive(builderState, t, star)
                 break
             case SaccharideShapes.FilledDiamond:
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
-                builder.add(t, octahedron)
+                MeshBuilder.addPrimitive(builderState, t, octahedron)
                 break
             case SaccharideShapes.DividedDiamond:
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
-                builder.add(t, perforatedOctahedron)
+                MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
                 Mat4.mul(t, t, Mat4.rotY90)
-                builder.setGroup(i * 2 + 1)
-                builder.add(t, perforatedOctahedron)
+                builderState.currentGroup = i * 2 + 1
+                MeshBuilder.addPrimitive(builderState, t, perforatedOctahedron)
                 break
             case SaccharideShapes.FlatDiamond:
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side / 2, side / 2))
-                builder.add(t, diamondPrism)
+                MeshBuilder.addPrimitive(builderState, t, diamondPrism)
                 break
             case SaccharideShapes.Pentagon:
                 Mat4.mul(t, t, Mat4.rotZY90)
                 Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
-                builder.add(t, pentagonalPrism)
+                MeshBuilder.addPrimitive(builderState, t, pentagonalPrism)
                 break
             case SaccharideShapes.FlatHexagon:
             default:
                 Mat4.mul(t, t, Mat4.rotZYZ90)
                 Mat4.scale(t, t, Vec3.set(sVec, side / 1.5, side , side / 2))
-                builder.add(t, hexagonalPrism)
+                MeshBuilder.addPrimitive(builderState, t, hexagonalPrism)
                 break
         }
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const CarbohydrateSymbolParams = {

+ 23 - 17
src/mol-repr/structure/visual/nucleotide-block-mesh.ts

@@ -8,8 +8,7 @@ import { Unit, Structure, ElementIndex } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { Segmentation } from 'mol-data/int';
-import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
-import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+import { isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
 import { NucleotideLocationIterator, markNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -20,6 +19,7 @@ import { addCylinder } from 'mol-geo/geometry/mesh/builder/cylinder';
 import { VisualContext } from 'mol-repr/representation';
 import { Theme } from 'mol-theme/theme';
 import { VisualUpdateState } from 'mol-repr/util';
+import { CylinderProps } from 'mol-geo/primitive/cylinder';
 
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
@@ -37,6 +37,7 @@ const box = Box()
 
 export const NucleotideBlockMeshParams = {
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
+    radialSegments: PD.Numeric(16, { min: 3, max: 56, step: 1 }),
 }
 export const DefaultNucleotideBlockMeshProps = PD.getDefaultValues(NucleotideBlockMeshParams)
 export type NucleotideBlockMeshProps = typeof DefaultNucleotideBlockMeshProps
@@ -44,31 +45,35 @@ export type NucleotideBlockMeshProps = typeof DefaultNucleotideBlockMeshProps
 function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: NucleotideBlockMeshProps, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
 
-    const { sizeFactor } = props
+    const nucleotideElementCount = unit.nucleotideElements.length
+    if (!nucleotideElementCount) return Mesh.createEmpty(mesh)
 
-    // TODO better vertex count estimate
-    const builder = MeshBuilder.create(256, 128, mesh)
+    const { sizeFactor, radialSegments } = props
+
+    const vertexCount = nucleotideElementCount * (box.vertices.length / 3 + radialSegments * 2)
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 4, mesh)
 
     const { elements, model } = unit
-    const { chemicalComponentMap, modifiedResidues } = model.properties
+    const { modifiedResidues } = model.properties
     const { chainAtomSegments, residueAtomSegments, residues, index: atomicIndex } = model.atomicHierarchy
+    const { moleculeType, traceElementIndex } = model.atomicHierarchy.derived.residue
     const { label_comp_id } = residues
     const pos = unit.conformation.invariantPosition
 
     const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
     const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
 
+    const cylinderProps: CylinderProps = { radiusTop: 1 * sizeFactor, radiusBottom: 1 * sizeFactor, radialSegments }
+
     let i = 0
     while (chainIt.hasNext) {
         residueIt.setSegment(chainIt.move());
 
         while (residueIt.hasNext) {
             const { index: residueIndex } = residueIt.move();
-            let compId = label_comp_id.value(residueIndex)
-            const cc = chemicalComponentMap.get(compId)
-            const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown
 
-            if (isNucleic(moleculeType)) {
+            if (isNucleic(moleculeType[residueIndex])) {
+                let compId = label_comp_id.value(residueIndex)
                 const parentId = modifiedResidues.parentId.get(compId)
                 if (parentId !== undefined) compId = parentId
                 let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1
@@ -81,7 +86,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
                     idx3 = atomicIndex.findAtomOnResidue(residueIndex, 'C6')
                     idx4 = atomicIndex.findAtomOnResidue(residueIndex, 'C2')
                     idx5 = atomicIndex.findAtomOnResidue(residueIndex, 'N9')
-                    idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
+                    idx6 = traceElementIndex[residueIndex]
                 } else if (isPyrimidineBase(compId)) {
                     height = 3.0
                     idx1 = atomicIndex.findAtomOnResidue(residueIndex, 'N3')
@@ -89,13 +94,13 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
                     idx3 = atomicIndex.findAtomOnResidue(residueIndex, 'C4')
                     idx4 = atomicIndex.findAtomOnResidue(residueIndex, 'C2')
                     idx5 = atomicIndex.findAtomOnResidue(residueIndex, 'N1')
-                    idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
+                    idx6 = traceElementIndex[residueIndex]
                 }
 
                 if (idx5 !== -1 && idx6 !== -1) {
                     pos(idx5, p5); pos(idx6, p6)
-                    builder.setGroup(i)
-                    addCylinder(builder, p5, p6, 1, { radiusTop: 1 * sizeFactor, radiusBottom: 1 * sizeFactor })
+                    builderState.currentGroup = i
+                    addCylinder(builderState, p5, p6, 1, cylinderProps)
                     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))
@@ -105,7 +110,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
                         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)
+                        MeshBuilder.addPrimitive(builderState, t, box)
                     }
                 }
 
@@ -114,7 +119,7 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
         }
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const NucleotideBlockParams = {
@@ -132,7 +137,8 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> {
         mark: markNucleotideElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NucleotideBlockParams>, currentProps: PD.Values<NucleotideBlockParams>) => {
             state.createGeometry = (
-                newProps.sizeFactor !== currentProps.sizeFactor
+                newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.radialSegments !== currentProps.radialSegments
             )
         }
     })

+ 6 - 6
src/mol-repr/structure/visual/polymer-backbone-cylinder.ts

@@ -35,7 +35,7 @@ function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, struc
     const { radialSegments, sizeFactor } = props
 
     const vertexCountEstimate = radialSegments * 2 * polymerElementCount * 2
-    const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
+    const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 10, mesh)
 
     const { elements } = unit
     const pos = unit.conformation.invariantPosition
@@ -50,15 +50,15 @@ function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, struc
         pos(centerB.element, pB)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA) * sizeFactor
-        builder.setGroup(OrderedSet.indexOf(elements, centerA.element))
-        addCylinder(builder, pA, pB, 0.5, cylinderProps)
+        builderState.currentGroup = OrderedSet.indexOf(elements, centerA.element)
+        addCylinder(builderState, pA, pB, 0.5, cylinderProps)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB) * sizeFactor
-        builder.setGroup(OrderedSet.indexOf(elements, centerB.element))
-        addCylinder(builder, pB, pA, 0.5, cylinderProps)
+        builderState.currentGroup = OrderedSet.indexOf(elements, centerB.element)
+        addCylinder(builderState, pB, pA, 0.5, cylinderProps)
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const PolymerBackboneParams = {

+ 4 - 4
src/mol-repr/structure/visual/polymer-direction-wedge.ts

@@ -43,7 +43,7 @@ function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, structu
     const { sizeFactor } = props
 
     const vertexCount = polymerElementCount * 24
-    const builder = MeshBuilder.create(vertexCount, vertexCount / 10, mesh)
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh)
     const linearSegments = 1
 
     const state = createCurveSegmentState(linearSegments)
@@ -53,7 +53,7 @@ function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, structu
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(i)
+        builderState.currentGroup = i
 
         const isNucleicType = isNucleic(v.moleculeType)
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -77,13 +77,13 @@ function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, structu
             Mat4.mul(t, t, Mat4.rotY90Z180)
             Mat4.scale(t, t, Vec3.set(sVec, height, width, depth))
             Mat4.setTranslation(t, v.p2)
-            builder.add(t, wedge)
+            MeshBuilder.addPrimitive(builderState, t, wedge)
         }
 
         ++i
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const PolymerDirectionParams = {

+ 8 - 8
src/mol-repr/structure/visual/polymer-gap-cylinder.ts

@@ -36,7 +36,7 @@ function createPolymerGapCylinderMesh(ctx: VisualContext, unit: Unit, structure:
     const { sizeFactor, radialSegments } = props
 
     const vertexCountEstimate = segmentCount * radialSegments * 2 * polymerGapCount * 2
-    const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
+    const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 10, mesh)
 
     const pos = unit.conformation.invariantPosition
     const pA = Vec3.zero()
@@ -50,26 +50,26 @@ function createPolymerGapCylinderMesh(ctx: VisualContext, unit: Unit, structure:
     while (polymerGapIt.hasNext) {
         const { centerA, centerB } = polymerGapIt.move()
         if (centerA.element === centerB.element) {
-            builder.setGroup(i)
+            builderState.currentGroup = i
             pos(centerA.element, pA)
-            addSphere(builder, pA, 0.6, 0)
+            addSphere(builderState, pA, 0.6, 0)
         } else {
             pos(centerA.element, pA)
             pos(centerB.element, pB)
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerA) * sizeFactor
-            builder.setGroup(i)
-            addFixedCountDashedCylinder(builder, pA, pB, 0.5, segmentCount, cylinderProps)
+            builderState.currentGroup = i
+            addFixedCountDashedCylinder(builderState, pA, pB, 0.5, segmentCount, cylinderProps)
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = theme.size.size(centerB) * sizeFactor
-            builder.setGroup(i + 1)
-            addFixedCountDashedCylinder(builder, pB, pA, 0.5, segmentCount, cylinderProps)
+            builderState.currentGroup = i + 1
+            addFixedCountDashedCylinder(builderState, pB, pA, 0.5, segmentCount, cylinderProps)
         }
 
         i += 2
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const InterUnitLinkParams = {

+ 5 - 5
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -37,7 +37,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
     const { sizeFactor, linearSegments, radialSegments, aspectRatio, arrowFactor } = props
 
     const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2
-    const builder = MeshBuilder.create(vertexCount, vertexCount / 10, mesh)
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh)
 
     const isCoarse = Unit.isCoarse(unit)
     const state = createCurveSegmentState(linearSegments)
@@ -47,7 +47,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(i)
+        builderState.currentGroup = i
 
         const isNucleicType = isNucleic(v.moleculeType)
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -63,7 +63,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
         if (isSheet) {
             const height = width * aspectRatio
             const arrowHeight = v.secStrucLast ? height * arrowFactor : 0
-            addSheet(builder, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, v.secStrucFirst, v.secStrucLast)
+            addSheet(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, v.secStrucFirst, v.secStrucLast)
         } else {
             let height: number
             if (isHelix) {
@@ -74,13 +74,13 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
             } else {
                 height = width
             }
-            addTube(builder, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, v.secStrucFirst, v.secStrucLast)
+            addTube(builderState, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, v.secStrucFirst, v.secStrucLast)
         }
 
         ++i
     }
 
-    return builder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export const PolymerTraceParams = {

+ 4 - 4
src/mol-repr/structure/visual/util/element.ts

@@ -29,7 +29,7 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
     const { elements } = unit;
     const elementCount = elements.length;
     const vertexCount = elementCount * sphereVertexCount(detail)
-    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
+    const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh)
 
     const v = Vec3.zero()
     const pos = unit.conformation.invariantPosition
@@ -40,11 +40,11 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
         l.element = elements[i]
         pos(elements[i], v)
 
-        meshBuilder.setGroup(i)
-        addSphere(meshBuilder, v, theme.size.size(l) * sizeFactor, detail)
+        builderState.currentGroup = i
+        addSphere(builderState, v, theme.size.size(l) * sizeFactor, detail)
     }
 
-    return meshBuilder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export function markElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {

+ 7 - 7
src/mol-repr/structure/visual/util/link.ts

@@ -74,7 +74,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
     const { linkScale, linkSpacing, radialSegments } = props
 
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2
-    const meshBuilder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 4, mesh)
+    const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh)
 
     const va = Vec3.zero()
     const vb = Vec3.zero()
@@ -87,12 +87,12 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
         const linkRadius = radius(edgeIndex)
         const o = order(edgeIndex)
         const f = flags(edgeIndex)
-        meshBuilder.setGroup(edgeIndex)
+        builderState.currentGroup = edgeIndex
 
         if (LinkType.is(f, LinkType.Flag.MetallicCoordination) || LinkType.is(f, LinkType.Flag.Hydrogen)) {
             // show metall coordinations and hydrogen bonds with dashed cylinders
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3
-            addFixedCountDashedCylinder(meshBuilder, va, vb, 0.5, 7, cylinderProps)
+            addFixedCountDashedCylinder(builderState, va, vb, 0.5, 7, cylinderProps)
         } else if (o === 2 || o === 3) {
             // show bonds with order 2 or 3 using 2 or 3 parallel cylinders
             const multiRadius = linkRadius * (linkScale / (0.5 * o))
@@ -103,15 +103,15 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkCyli
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius
 
-            if (o === 3) addCylinder(meshBuilder, va, vb, 0.5, cylinderProps)
-            addDoubleCylinder(meshBuilder, va, vb, 0.5, vShift, cylinderProps)
+            if (o === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps)
+            addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps)
         } else {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius
-            addCylinder(meshBuilder, va, vb, 0.5, cylinderProps)
+            addCylinder(builderState, va, vb, 0.5, cylinderProps)
         }
     }
 
-    return meshBuilder.getMesh()
+    return MeshBuilder.getMesh(builderState)
 }
 
 export namespace LinkIterator {

+ 3 - 3
src/mol-repr/structure/visual/util/nucleotide.ts

@@ -12,7 +12,6 @@ import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { StructureGroup } from 'mol-repr/structure/units-visual';
 import { getResidueLoci } from './common';
-import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 
 export namespace NucleotideLocationIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
@@ -50,6 +49,7 @@ export function markNucleotideElement(loci: Loci, structureGroup: StructureGroup
     if (!Unit.isAtomic(unit)) return false
     const { nucleotideElements, model, elements } = unit
     const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+    const { traceElementIndex } = model.atomicHierarchy.derived.residue
     const groupCount = nucleotideElements.length
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
@@ -61,8 +61,8 @@ export function markNucleotideElement(loci: Loci, structureGroup: StructureGroup
                 const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
                 const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
                 const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if(!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-                const eI = getElementIndexForAtomRole(model, rI, 'trace')
+                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                const eI = traceElementIndex[rI]
                 const idx = OrderedSet.indexOf(eUnit.nucleotideElements, eI)
                 if (idx !== -1) {
                     if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true

+ 3 - 3
src/mol-repr/structure/visual/util/polymer.ts

@@ -11,7 +11,6 @@ import { EmptyLoci, Loci } from 'mol-model/loci';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { StructureGroup } from 'mol-repr/structure/units-visual';
-import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getResidueLoci } from './common';
 
 export * from './polymer/backbone-iterator'
@@ -86,6 +85,7 @@ export function markPolymerElement(loci: Loci, structureGroup: StructureGroup, a
     if (loci.structure !== structure) return false
     const { polymerElements, model, elements } = group.units[0]
     const { index, offsets } = model.atomicHierarchy.residueAtomSegments
+    const { traceElementIndex } = model.atomicHierarchy.derived.residue
     const groupCount = polymerElements.length
     for (const e of loci.elements) {
         const unitIdx = group.unitIndexMap.get(e.unit.id)
@@ -96,8 +96,8 @@ export function markPolymerElement(loci: Loci, structureGroup: StructureGroup, a
                 const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
                 const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
                 const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if(!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
-                const eI = getElementIndexForAtomRole(model, rI, 'trace')
+                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                const eI = traceElementIndex[rI]
                 const idx = OrderedSet.indexOf(e.unit.polymerElements, eI)
                 if (idx !== -1) {
                     if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true

+ 5 - 11
src/mol-repr/structure/visual/util/polymer/backbone-iterator.ts

@@ -8,9 +8,7 @@ import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/st
 import { Segmentation } from 'mol-data/int';
 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> {
@@ -37,6 +35,7 @@ function createPolymerBackbonePair (unit: Unit) {
 const enum AtomicPolymerBackboneIteratorState { nextPolymer, firstResidue, nextResidue, cycle }
 
 export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePair> {
+    private traceElementIndex: ArrayLike<ElementIndex>
     private value: PolymerBackbonePair
     private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
     private residueIt: Segmentation.SegmentIterator<ResidueIndex>
@@ -44,19 +43,13 @@ 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 = this.getElementIndex(this.residueSegment.index, 'trace')
+                    this.value.centerB.element = this.traceElementIndex[this.residueSegment.index]
                     this.state = AtomicPolymerBackboneIteratorState.nextResidue
                     break
                 }
@@ -66,7 +59,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 = this.getElementIndex(this.residueSegment.index, 'trace')
+            this.value.centerB.element = this.traceElementIndex[this.residueSegment.index]
             if (!this.residueIt.hasNext) {
                 if (this.unit.model.atomicHierarchy.cyclicPolymerMap.has(this.residueSegment.index)) {
                     this.state = AtomicPolymerBackboneIteratorState.cycle
@@ -78,7 +71,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 = this.getElementIndex(cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
+            this.value.centerB.element = this.traceElementIndex[cyclicPolymerMap.get(this.residueSegment.index)!]
             // TODO need to advance to a polymer that has two or more residues (can't assume it has)
             this.state = AtomicPolymerBackboneIteratorState.nextPolymer
         }
@@ -88,6 +81,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
     }
 
     constructor(private unit: Unit.Atomic) {
+        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex
         this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements)
         this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements)
         this.value = createPolymerBackbonePair(unit)

+ 4 - 9
src/mol-repr/structure/visual/util/polymer/gap-iterator.ts

@@ -5,10 +5,8 @@
  */
 
 import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/structure';
-import { AtomRole } from 'mol-model/structure/model/types';
 import Iterator from 'mol-data/iterator';
 import SortedRanges from 'mol-data/int/sorted-ranges';
-import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getGapRanges } from '../polymer';
 
 /** Iterates over gaps, i.e. the stem residues/coarse elements adjacent to gaps */
@@ -34,25 +32,22 @@ function createPolymerGapPair (unit: Unit) {
 }
 
 export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> {
+    private traceElementIndex: ArrayLike<ElementIndex>
     private value: PolymerGapPair
     private gapIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
     hasNext: boolean = false;
 
-    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
-        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
-        return elementIndex === -1 ? this.unit.model.atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
-    }
-
     move() {
         const { elements, residueIndex } = this.unit
         const gapSegment = this.gapIt.move();
-        this.value.centerA.element = this.getElementIndex(residueIndex[elements[gapSegment.start]], 'trace')
-        this.value.centerB.element = this.getElementIndex(residueIndex[elements[gapSegment.end - 1]], 'trace')
+        this.value.centerA.element = this.traceElementIndex[residueIndex[elements[gapSegment.start]]]
+        this.value.centerB.element = this.traceElementIndex[residueIndex[elements[gapSegment.end - 1]]]
         this.hasNext = this.gapIt.hasNext
         return this.value;
     }
 
     constructor(private unit: Unit.Atomic) {
+        this.traceElementIndex = unit.model.atomicHierarchy.derived.residue.traceElementIndex
         this.gapIt = SortedRanges.transientSegments(getGapRanges(unit), unit.elements);
         this.value = createPolymerGapPair(unit)
         this.hasNext = this.gapIt.hasNext

+ 10 - 4
src/mol-theme/color.ts

@@ -55,8 +55,9 @@ namespace ColorTheme {
         readonly label: string
         readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
         readonly getParams: (ctx: ThemeDataContext) => P
+        readonly defaultValues: PD.Values<P>
     }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) }
+    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {} }
 
     export class Registry {
         private _list: { name: string, provider: Provider<any> }[] = []
@@ -79,13 +80,18 @@ namespace ColorTheme {
             this._map.set(name, provider)
         }
 
+        remove(name: string) {
+            this._list.splice(this._list.findIndex(e => e.name === name))
+            this._map.delete(name)
+        }
+
         get<P extends PD.Params>(name: string): Provider<P> {
             return this._map.get(name) || EmptyProvider as unknown as Provider<P>
         }
 
-        create(id: string, ctx: ThemeDataContext, props = {}) {
-            const provider = this.get(id)
-            return provider ? provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props }) : Empty
+        create(name: string, ctx: ThemeDataContext, props = {}) {
+            const provider = this.get(name)
+            return provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props })
         }
 
         get list() {

+ 2 - 1
src/mol-theme/color/carbohydrate-symbol.ts

@@ -77,5 +77,6 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Va
 export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = {
     label: 'Carbohydrate Symbol',
     factory: CarbohydrateSymbolColorTheme,
-    getParams: getCarbohydrateSymbolColorThemeParams
+    getParams: getCarbohydrateSymbolColorThemeParams,
+    defaultValues: PD.getDefaultValues(CarbohydrateSymbolColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/chain-id.ts

@@ -94,5 +94,6 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainI
 export const ChainIdColorThemeProvider: ColorTheme.Provider<ChainIdColorThemeParams> = {
     label: 'Chain Id',
     factory: ChainIdColorTheme,
-    getParams: getChainIdColorThemeParams
+    getParams: getChainIdColorThemeParams,
+    defaultValues: PD.getDefaultValues(ChainIdColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/cross-link.ts

@@ -71,5 +71,6 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<Cros
 export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = {
     label: 'Cross Link',
     factory: CrossLinkColorTheme,
-    getParams: getCrossLinkColorThemeParams
+    getParams: getCrossLinkColorThemeParams,
+    defaultValues: PD.getDefaultValues(CrossLinkColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/element-index.ts

@@ -71,5 +71,6 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<E
 export const ElementIndexColorThemeProvider: ColorTheme.Provider<ElementIndexColorThemeParams> = {
     label: 'Element Index',
     factory: ElementIndexColorTheme,
-    getParams: getElementIndexColorThemeParams
+    getParams: getElementIndexColorThemeParams,
+    defaultValues: PD.getDefaultValues(ElementIndexColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/element-symbol.ts

@@ -63,5 +63,6 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
 export const ElementSymbolColorThemeProvider: ColorTheme.Provider<ElementSymbolColorThemeParams> = {
     label: 'Element Symbol',
     factory: ElementSymbolColorTheme,
-    getParams: getElementSymbolColorThemeParams
+    getParams: getElementSymbolColorThemeParams,
+    defaultValues: PD.getDefaultValues(ElementSymbolColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/molecule-type.ts

@@ -72,5 +72,6 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<M
 export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<MoleculeTypeColorThemeParams> = {
     label: 'Molecule Type',
     factory: MoleculeTypeColorTheme,
-    getParams: getMoleculeTypeColorThemeParams
+    getParams: getMoleculeTypeColorThemeParams,
+    defaultValues: PD.getDefaultValues(MoleculeTypeColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/polymer-id.ts

@@ -101,5 +101,6 @@ export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Poly
 export const PolymerIdColorThemeProvider: ColorTheme.Provider<PolymerIdColorThemeParams> = {
     label: 'Polymer Id',
     factory: PolymerIdColorTheme,
-    getParams: getPolymerIdColorThemeParams
+    getParams: getPolymerIdColorThemeParams,
+    defaultValues: PD.getDefaultValues(PolymerIdColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/polymer-index.ts

@@ -69,5 +69,6 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<P
 export const PolymerIndexColorThemeProvider: ColorTheme.Provider<PolymerIndexColorThemeParams> = {
     label: 'Polymer Index',
     factory: PolymerIndexColorTheme,
-    getParams: getPolymerIndexColorThemeParams
+    getParams: getPolymerIndexColorThemeParams,
+    defaultValues: PD.getDefaultValues(PolymerIndexColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/residue-name.ts

@@ -128,5 +128,6 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<Re
 export const ResidueNameColorThemeProvider: ColorTheme.Provider<ResidueNameColorThemeParams> = {
     label: 'Residue Name',
     factory: ResidueNameColorTheme,
-    getParams: getResidueNameColorThemeParams
+    getParams: getResidueNameColorThemeParams,
+    defaultValues: PD.getDefaultValues(ResidueNameColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/secondary-structure.ts

@@ -95,5 +95,6 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Va
 export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<SecondaryStructureColorThemeParams> = {
     label: 'Secondary Structure',
     factory: SecondaryStructureColorTheme,
-    getParams: getSecondaryStructureColorThemeParams
+    getParams: getSecondaryStructureColorThemeParams,
+    defaultValues: PD.getDefaultValues(SecondaryStructureColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/sequence-id.ts

@@ -102,5 +102,6 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<Seq
 export const SequenceIdColorThemeProvider: ColorTheme.Provider<SequenceIdColorThemeParams> = {
     label: 'Sequence Id',
     factory: SequenceIdColorTheme,
-    getParams: getSequenceIdColorThemeParams
+    getParams: getSequenceIdColorThemeParams,
+    defaultValues: PD.getDefaultValues(SequenceIdColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/shape-group.ts

@@ -38,5 +38,6 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha
 export const ShapeGroupColorThemeProvider: ColorTheme.Provider<ShapeGroupColorThemeParams> = {
     label: 'Shape Group',
     factory: ShapeGroupColorTheme,
-    getParams: getShapeGroupColorThemeParams
+    getParams: getShapeGroupColorThemeParams,
+    defaultValues: PD.getDefaultValues(ShapeGroupColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/uniform.ts

@@ -37,5 +37,6 @@ export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<Unifor
 export const UniformColorThemeProvider: ColorTheme.Provider<UniformColorThemeParams> = {
     label: 'Uniform',
     factory: UniformColorTheme,
-    getParams: getUniformColorThemeParams
+    getParams: getUniformColorThemeParams,
+    defaultValues: PD.getDefaultValues(UniformColorThemeParams)
 }

+ 2 - 1
src/mol-theme/color/unit-index.ts

@@ -60,5 +60,6 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Unit
 export const UnitIndexColorThemeProvider: ColorTheme.Provider<UnitIndexColorThemeParams> = {
     label: 'Unit Index',
     factory: UnitIndexColorTheme,
-    getParams: getUnitIndexColorThemeParams
+    getParams: getUnitIndexColorThemeParams,
+    defaultValues: PD.getDefaultValues(UnitIndexColorThemeParams)
 }

+ 12 - 6
src/mol-theme/size.ts

@@ -33,8 +33,9 @@ namespace SizeTheme {
         readonly label: string
         readonly factory: Factory<P>
         readonly getParams: (ctx: ThemeDataContext) => P
+        readonly defaultValues: PD.Values<P>
     }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) }
+    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}), defaultValues: {} }
 
     export class Registry {
         private _list: { name: string, provider: Provider<any> }[] = []
@@ -57,13 +58,18 @@ namespace SizeTheme {
             this._map.set(name, provider)
         }
 
-        get<P extends PD.Params>(id: string) {
-            return this._map.get(id) || EmptyProvider as unknown as Provider<P>
+        remove(name: string) {
+            this._list.splice(this._list.findIndex(e => e.name === name))
+            this._map.delete(name)
         }
 
-        create(id: string, ctx: ThemeDataContext, props = {}) {
-            const provider = this.get(id)
-            return provider ? provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props }) : Empty
+        get<P extends PD.Params>(name: string): Provider<P> {
+            return this._map.get(name) || EmptyProvider as unknown as Provider<P>
+        }
+
+        create(name: string, ctx: ThemeDataContext, props = {}) {
+            const provider = this.get(name)
+            return provider.factory(ctx, { ...PD.getDefaultValues(provider.getParams(ctx)), ...props })
         }
 
         get list() {

+ 2 - 1
src/mol-theme/size/physical.ts

@@ -59,5 +59,6 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<Physic
 export const PhysicalSizeThemeProvider: SizeTheme.Provider<PhysicalSizeThemeParams> = {
     label: 'Physical',
     factory: PhysicalSizeTheme,
-    getParams: getPhysicalSizeThemeParams
+    getParams: getPhysicalSizeThemeParams,
+    defaultValues: PD.getDefaultValues(PhysicalSizeThemeParams)
 }

+ 2 - 1
src/mol-theme/size/uniform.ts

@@ -33,5 +33,6 @@ export function UniformSizeTheme(ctx: ThemeDataContext, props: PD.Values<Uniform
 export const UniformSizeThemeProvider: SizeTheme.Provider<UniformSizeThemeParams> = {
     label: 'Uniform',
     factory: UniformSizeTheme,
-    getParams: getUniformSizeThemeParams
+    getParams: getUniformSizeThemeParams,
+    defaultValues: PD.getDefaultValues(UniformSizeThemeParams)
 }

+ 7 - 4
src/mol-util/data-source.ts

@@ -19,6 +19,7 @@ export interface AjaxGetParams {
     type: 'string' | 'binary',
     title?: string,
     compression?: DataCompressionMethod
+    body?: string
 }
 
 export function readStringFromFile(file: File) {
@@ -42,9 +43,11 @@ export function ajaxGetUint8Array(url: string, title?: string) {
 }
 
 export function ajaxGet(params: AjaxGetParams) {
-    return <Task<string | Uint8Array>>ajaxGetInternal(params.title, params.url, params.type === 'binary', params.compression === DataCompressionMethod.Gzip);
+    return <Task<string | Uint8Array>>ajaxGetInternal(params.title, params.url, params.type === 'binary', params.compression === DataCompressionMethod.Gzip, params.body);
 }
 
+export type AjaxTask = (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>
+
 function decompress(buffer: Uint8Array): Uint8Array {
     // TODO
     throw 'nyi';
@@ -160,7 +163,7 @@ async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompres
     }
 }
 
-function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: boolean, decompressGzip: boolean): Task<string | Uint8Array> {
+function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: boolean, decompressGzip: boolean, body?: string): Task<string | Uint8Array> {
     let xhttp: XMLHttpRequest | undefined = void 0;
     return Task.create(title ? title : 'Download', async ctx => {
         try {
@@ -170,9 +173,9 @@ function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: b
 
             xhttp = RequestPool.get();
 
-            xhttp.open('get', url, true);
+            xhttp.open(body ? 'post' : 'get', url, true);
             xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text';
-            xhttp.send();
+            xhttp.send(body);
 
             ctx.update({ message: 'Waiting for server...', canAbort: true });
             const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array);

+ 85 - 0
src/mol-util/graphql-client.ts

@@ -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>
+ *
+ * Adapted from https://github.com/prisma/graphql-request, Copyright (c) 2017 Graphcool, MIT
+ */
+
+import { Task, RuntimeContext } from 'mol-task';
+
+type Variables = { [key: string]: any }
+
+interface GraphQLError {
+    message: string
+    locations: { line: number, column: number }[]
+    path: string[]
+}
+
+interface GraphQLResponse {
+    data?: any
+    errors?: GraphQLError[]
+    extensions?: any
+    status: number
+    [key: string]: any
+}
+
+interface GraphQLRequestContext {
+    query: string
+    variables?: Variables
+}
+
+export class ClientError extends Error {
+    response: GraphQLResponse
+    request: GraphQLRequestContext
+
+    constructor (response: GraphQLResponse, request: GraphQLRequestContext) {
+        const message = `${ClientError.extractMessage(response)}: ${JSON.stringify({ response, request })}`
+
+        super(message)
+
+        this.response = response
+        this.request = request
+
+        // this is needed as Safari doesn't support .captureStackTrace
+        /* tslint:disable-next-line */
+        if (typeof Error.captureStackTrace === 'function') {
+            Error.captureStackTrace(this, ClientError)
+        }
+    }
+
+    private static extractMessage (response: GraphQLResponse): string {
+        try {
+            return response.errors![0].message
+        } catch (e) {
+            return `GraphQL Error (Code: ${response.status})`
+        }
+    }
+}
+
+export class GraphQLClient {
+    constructor(private url: string, private fetch: (url: string, type: 'string' | 'binary', body?: string) => Task<string | Uint8Array>) {
+        this.url = url
+    }
+
+    async request<T extends any>(ctx: RuntimeContext, query: string, variables?: Variables): Promise<T> {
+
+        const body = JSON.stringify({
+            query,
+            variables: variables ? variables : undefined,
+        })
+
+        const resultStr = await this.fetch(this.url, 'string', body).runInContext(ctx) as string
+        const result = JSON.parse(resultStr)
+
+        if (!result.errors && result.data) {
+            return result.data
+        } else {
+            const errorResult = typeof result === 'string' ? { error: result } : result
+            throw new ClientError(
+                { ...errorResult },
+                { query, variables },
+            )
+        }
+    }
+}

+ 2 - 2
src/mol-util/param-definition.ts

@@ -42,12 +42,12 @@ export namespace ParamDefinition {
         return setInfo<Value<T>>({ type: 'value', defaultValue }, info);
     }
 
-    export interface Select<T extends string> extends Base<T> {
+    export interface Select<T extends string | number> extends Base<T> {
         type: 'select'
         /** array of (value, label) tuples */
         options: [T, string][]
     }
-    export function Select<T extends string>(defaultValue: T, options: [T, string][], info?: Info): Select<T> {
+    export function Select<T extends string | number>(defaultValue: T, options: [T, string][], info?: Info): Select<T> {
         return setInfo<Select<T>>({ type: 'select', defaultValue, options }, info)
     }
 

+ 1 - 1
src/servers/model/properties/providers/rcsb.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { AssemblySymmetry } from 'mol-model-props/rcsb/assembly-symmetry';
 import { AttachModelProperty } from '../../property-provider';
 
 export const RCSB_assemblySymmetry: AttachModelProperty = ({ model }) => {