diff --git a/package-lock.json b/package-lock.json
index 4d46805a5edccbc2fb5c26a992a06e77ac016ed1..63ac0178bd15b50ad3c918ff905ffecf351dbaf9 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index 31377fcba688342aebbb61ffdffcbdbf544d1ce1..8d149012463de799160e12c086b3f8724f97eb26 100644
--- a/package.json
+++ b/package.json
@@ -76,9 +76,9 @@
     "@types/compression": "0.0.36",
     "@types/express": "^4.16.0",
     "@types/jest": "^23.3.1",
-    "@types/node": "^10.7.1",
+    "@types/node": "^10.9.4",
     "@types/node-fetch": "^2.1.2",
-    "@types/react": "^16.4.11",
+    "@types/react": "^16.4.13",
     "@types/react-dom": "^16.0.7",
     "benchmark": "^2.1.4",
     "cpx": "^1.5.0",
@@ -97,11 +97,11 @@
     "raw-loader": "^0.5.1",
     "resolve-url-loader": "^2.3.0",
     "sass-loader": "^7.1.0",
-    "style-loader": "^0.22.1",
+    "style-loader": "^0.23.0",
     "ts-jest": "^23.1.4",
     "tslint": "^5.11.0",
-    "typescript": "^3.0.1",
-    "uglify-js": "^3.4.7",
+    "typescript": "^3.0.3",
+    "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
     "webpack": "^4.17.1",
     "webpack-cli": "^3.1.0"
@@ -116,6 +116,6 @@
     "node-fetch": "^2.2.0",
     "react": "^16.4.2",
     "react-dom": "^16.4.2",
-    "rxjs": "^6.2.2"
+    "rxjs": "^6.3.1"
   }
 }
diff --git a/src/apps/canvas/app.ts b/src/apps/canvas/app.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9aeeccf00a3c9a622834b2372c37c854d62eef24
--- /dev/null
+++ b/src/apps/canvas/app.ts
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Viewer from 'mol-view/viewer';
+import { getCifFromUrl, getModelsFromMmcif, getCifFromFile } from './util';
+import { StructureView } from './structure-view';
+import { BehaviorSubject } from 'rxjs';
+import { CifBlock } from 'mol-io/reader/cif';
+
+export class App {
+    viewer: Viewer
+    container: HTMLDivElement | null = null;
+    canvas: HTMLCanvasElement | null = null;
+    structureView: StructureView | null = null;
+
+    pdbIdLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null)
+
+    initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) {
+        this.canvas = _canvas
+        this.container = _container
+
+        try {
+            this.viewer = Viewer.create(this.canvas, this.container)
+            this.viewer.animate()
+            return true
+        } catch (e) {
+            console.error(e)
+            return false
+        }
+    }
+
+    async loadCif(cif: CifBlock, assemblyId?: string) {
+        const models = await getModelsFromMmcif(cif)
+        this.structureView = await StructureView(this.viewer, models, { assemblyId })
+        this.pdbIdLoaded.next(this.structureView)
+    }
+
+    async loadPdbId(id: string, assemblyId?: string) {
+        if (this.structureView) this.structureView.destroy()
+        const cif = await getCifFromUrl(`https://files.rcsb.org/download/${id}.cif`)
+        this.loadCif(cif, assemblyId)
+    }
+
+    async loadCifFile(file: File) {
+        if (this.structureView) this.structureView.destroy()
+        const cif = await getCifFromFile(file)
+        this.loadCif(cif)
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/assembly-symmetry.ts b/src/apps/canvas/assembly-symmetry.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69fee7dd07fd6acfb60c87a354f719dd556a2493
--- /dev/null
+++ b/src/apps/canvas/assembly-symmetry.ts
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { Table } from 'mol-data/db';
+import { Color, ColorScale } from 'mol-util/color';
+import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
+import { Tensor } from 'mol-math/linear-algebra';
+import { addSphere } from 'mol-geo/mesh/builder/sphere';
+import { addCylinder } from 'mol-geo/mesh/builder/cylinder';
+import { Shape } from 'mol-model/shape';
+import { ColorTheme } from 'mol-view/theme/color';
+import { Location } from 'mol-model/location';
+import { StructureElement, Unit, StructureProperties } from 'mol-model/structure';
+
+export function getAxesShape(featureId: number, assemblySymmetry: AssemblySymmetry) {
+    const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature
+    const feature = Table.pickRow(f, i => f.id.value(i) === featureId)
+    if (!feature) return
+
+    const axes = assemblySymmetry.getAxes(featureId)
+    if (!axes._rowCount) return
+
+    const vectorSpace = AssemblySymmetry.Schema.rcsb_assembly_symmetry_axis.start.space;
+
+    const colors: Color[] = []
+    const labels: string[] = []
+
+    const radius = 0.4
+    const cylinderProps = { radiusTop: radius, radiusBottom: radius }
+    const meshBuilder = MeshBuilder.create(256, 128)
+
+    for (let i = 0, il = axes._rowCount; i < il; ++i) {
+        const start = Tensor.toVec3(vectorSpace, axes.start.value(i))
+        const end = Tensor.toVec3(vectorSpace, axes.end.value(i))
+        meshBuilder.setGroup(i)
+        addSphere(meshBuilder, start, radius, 2)
+        addSphere(meshBuilder, end, radius, 2)
+        addCylinder(meshBuilder, start, end, 1, cylinderProps)
+        colors.push(Color(0xCCEE11))
+        labels.push(`Axis ${i + 1} for ${feature.symmetry_value} ${feature.type.toLowerCase()} symmetry`)
+    }
+    const mesh = meshBuilder.getMesh()
+    const shape = Shape.create('Axes', mesh, colors, labels)
+    return shape
+}
+
+function getAsymId(unit: Unit): StructureElement.Property<string> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            return StructureProperties.chain.auth_asym_id // TODO
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return StructureProperties.coarse.asym_id
+    }
+}
+
+function memberKey (asym_id: string, oper_list_id?: number) {
+    return `${asym_id}|${oper_list_id}`
+}
+
+export function getClusterColorTheme(featureId: number, assemblySymmetry: AssemblySymmetry): ColorTheme {
+    const DefaultColor = Color(0xCCCCCC)
+    const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature
+    const feature = Table.pickRow(f, i => f.id.value(i) === featureId)
+    if (!feature) return { granularity: 'uniform', color: () => DefaultColor }
+
+    const clusters = assemblySymmetry.getClusters(featureId)
+    if (!clusters._rowCount) return { granularity: 'uniform', color: () => DefaultColor }
+
+    const clusterByMember = new Map<string, number>()
+    for (let i = 0, il = clusters._rowCount; i < il; ++i) {
+        clusters.members.value(i).forEach(m => {
+            const ms = m.split('_')
+            const asym_id = ms[0]
+            const oper_list_id = ms.length === 2 ? parseInt(ms[1]) : undefined
+            clusterByMember.set(memberKey(asym_id, oper_list_id), i)
+        })
+    }
+
+    const scale = ColorScale.create({ domain: [ 0, clusters._rowCount - 1 ] })
+
+    return {
+        granularity: 'instance',
+        color: (location: Location): Color => {
+            if (StructureElement.isLocation(location)) {
+                const ns = location.unit.conformation.operator.name.split('-')
+                const asym_id = getAsymId(location.unit)
+                const oper_list_id = ns.length === 2 ? parseInt(ns[1]) : undefined
+                const cluster = clusterByMember.get(memberKey(asym_id(location), oper_list_id))
+                return cluster !== undefined ? scale.color(cluster) : DefaultColor
+            }
+            return DefaultColor
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/app.tsx b/src/apps/canvas/component/app.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6fe5f0872a62a8a5d552767872e1798c8c845426
--- /dev/null
+++ b/src/apps/canvas/component/app.tsx
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../structure-view';
+import { App } from '../app';
+import { Viewport } from './viewport';
+import { StructureViewComponent } from './structure-view';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface AppProps {
+    app: App
+}
+
+export interface AppState {
+    structureView: StructureView | null
+}
+
+export class AppComponent extends React.Component<AppProps, AppState> {
+    state = {
+        structureView: this.props.app.structureView,
+    }
+
+    componentDidMount() {
+        this.props.app.pdbIdLoaded.subscribe((structureView) => {
+            this.setState({
+                structureView: this.props.app.structureView
+            })
+        })
+    }
+
+    render() {
+        const { structureView } = this.state
+
+        return <div style={{width: '100%', height: '100%'}}>
+            <div style={{left: '0px', right: '350px', height: '100%', position: 'absolute'}}>
+                <Viewport app={this.props.app} />
+            </div>
+
+            <div style={{width: '330px', paddingLeft: '10px', paddingRight: '10px', right: '0px', height: '100%', position: 'absolute', overflow: 'auto'}}>
+                <div style={{marginTop: '10px'}}>
+                    <span>Load PDB ID </span>
+                    <input
+                        type='text'
+                        onKeyDown={e => {
+                            if (e.keyCode === 13) {
+                                const value = e.currentTarget.value.trim()
+                                if (value) {
+                                    this.props.app.loadPdbId(value)
+                                }
+                            }
+                        }}
+                    />
+                </div>
+                <div>
+                    <span>Load CIF file </span>
+                    <input
+                        accept='*.cif'
+                        type='file'
+                        onChange={e => {
+                            if (e.target.files) this.props.app.loadCifFile(e.target.files[0])
+                        }}
+                    />
+                </div>
+                <hr/>
+                <div style={{marginBottom: '10px'}}>
+                    {structureView ? <StructureViewComponent structureView={structureView} /> : ''}
+                </div>
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/structure-representation.tsx b/src/apps/canvas/component/structure-representation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..25eb022ae279bbae55b8af05fc50cfc8a369d901
--- /dev/null
+++ b/src/apps/canvas/component/structure-representation.tsx
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureRepresentation, StructureProps } from 'mol-geo/representation/structure';
+import Viewer from 'mol-view/viewer';
+import { VisualQuality, VisualQualityNames } from 'mol-geo/representation/util';
+import { ColorThemeProps, ColorThemeName, ColorThemeNames, ColorTheme } from 'mol-view/theme/color';
+import { Color } from 'mol-util/color';
+
+export interface StructureRepresentationComponentProps {
+    viewer: Viewer
+    representation: StructureRepresentation<StructureProps>
+}
+
+export interface StructureRepresentationComponentState {
+    label: string
+    visible: boolean
+    quality: VisualQuality
+    colorTheme: ColorThemeProps
+}
+
+export class StructureRepresentationComponent extends React.Component<StructureRepresentationComponentProps, StructureRepresentationComponentState> {
+    state = {
+        label: this.props.representation.label,
+        visible: this.props.representation.props.visible,
+        quality: this.props.representation.props.quality,
+        colorTheme: this.props.representation.props.colorTheme,
+    }
+
+    componentWillMount() {
+        const repr = this.props.representation
+
+        this.setState({
+            ...this.state,
+            label: repr.label,
+            visible: repr.props.visible,
+            quality: repr.props.quality,
+            colorTheme: repr.props.colorTheme,
+        })
+    }
+
+    async update(state: Partial<StructureRepresentationComponentState>) {
+        const repr = this.props.representation
+        const props: Partial<StructureProps> = {}
+
+        if (state.visible !== undefined) props.visible = state.visible
+        if (state.quality !== undefined) props.quality = state.quality
+        if (state.colorTheme !== undefined) props.colorTheme = state.colorTheme
+
+        await repr.createOrUpdate(props).run()
+        this.props.viewer.add(repr)
+        this.props.viewer.requestDraw(true)
+        console.log(this.props.viewer.stats)
+
+        const newState = {
+            ...this.state,
+            visible: repr.props.visible,
+            quality: repr.props.quality,
+            colorTheme: repr.props.colorTheme,
+        }
+        this.setState(newState)
+    }
+
+    render() {
+        const { label, visible, quality, colorTheme } = this.state
+
+        const ct = ColorTheme(colorTheme)
+
+        if (ct.legend && ct.legend.kind === 'scale-legend') {
+            // console.log(`linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`)
+        }
+
+        return <div>
+            <div>
+                <h4>{label}</h4>
+            </div>
+            <div>
+                <div>
+                    <span>Visible </span>
+                    <button onClick={(e) => this.update({ visible: !visible }) }>
+                        {visible ? 'Hide' : 'Show'}
+                    </button>
+                </div>
+                <div>
+                    <span>Quality </span>
+                    <select value={quality} onChange={(e) => this.update({ quality: e.target.value as VisualQuality }) }>
+                        {VisualQualityNames.map(name => <option key={name} value={name}>{name}</option>)}
+                    </select>
+                </div>
+                <div>
+                    <span>Color Theme </span>
+                    <select value={colorTheme.name} onChange={(e) => this.update({ colorTheme: { name: e.target.value as ColorThemeName } }) }>
+                        {ColorThemeNames.map(name => <option key={name} value={name}>{name}</option>)}
+                    </select>
+                    {ct.description ? <div><i>{ct.description}</i></div> : ''}
+                    {
+                        ct.legend && ct.legend.kind === 'scale-legend'
+                            ? <div
+                                style={{
+                                    width: '100%',
+                                    height: '30px',
+                                    background: `linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`
+                                }}
+                            >
+                                <span style={{float: 'left', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.min}</span>
+                                <span style={{float: 'right', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.max}</span>
+                            </div>
+                        : ct.legend && ct.legend.kind === 'table-legend'
+                            ? <div>
+                                {ct.legend.table.map((value, i) => {
+                                    const [name, color] = value
+                                    return <div key={i} style={{minWidth: '60px', marginRight: '5px', display: 'inline-block'}}>
+                                        <div style={{width: '30px', height: '20px', backgroundColor: Color.toStyle(color), display: 'inline-block'}}></div>
+                                        {name}
+                                    </div>
+                                })}
+                            </div>
+                        : ''
+                    }
+                </div>
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/structure-view.tsx b/src/apps/canvas/component/structure-view.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6f05f7403c1ed359535db6d9707f79aec47ec424
--- /dev/null
+++ b/src/apps/canvas/component/structure-view.tsx
@@ -0,0 +1,198 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { StructureView } from '../structure-view';
+import { StructureRepresentation } from 'mol-geo/representation/structure';
+import { StructureRepresentationComponent } from './structure-representation';
+
+// export function FileInput (props: {
+//     accept: string
+//     onChange: (v: FileList | null) => void,
+// }) {
+//     return <input
+//         accept={props.accept || '*.*'}
+//         type='file'
+//         onChange={e => props.onChange.call(null, e.target.files)}
+//     />
+// }
+
+export interface StructureViewComponentProps {
+    structureView: StructureView
+}
+
+export interface StructureViewComponentState {
+    structureView: StructureView
+
+    label: string
+    modelId: number
+    modelIds: { id: number, label: string }[]
+    assemblyId: string
+    assemblyIds: { id: string, label: string }[]
+    symmetryFeatureId: number
+    symmetryFeatureIds: { id: number, label: string }[]
+
+    active: { [k: string]: boolean }
+    structureRepresentations: { [k: string]: StructureRepresentation<any> }
+}
+
+export class StructureViewComponent extends React.Component<StructureViewComponentProps, StructureViewComponentState> {
+    state = this.stateFromStructureView(this.props.structureView)
+
+    private stateFromStructureView(sv: StructureView) {
+        return {
+            structureView: sv,
+
+            label: sv.label,
+            modelId: sv.modelId,
+            modelIds: sv.getModelIds(),
+            assemblyId: sv.assemblyId,
+            assemblyIds: sv.getAssemblyIds(),
+            symmetryFeatureId: sv.symmetryFeatureId,
+            symmetryFeatureIds: sv.getSymmetryFeatureIds(),
+
+            active: sv.active,
+            structureRepresentations: sv.structureRepresentations
+        }
+    }
+
+    componentWillMount() {
+        this.setState(this.stateFromStructureView(this.props.structureView))
+    }
+
+    componentDidMount() {
+        const sv = this.props.structureView
+
+        this.props.structureView.updated.subscribe(() => this.setState({
+            symmetryFeatureIds: sv.getSymmetryFeatureIds(),
+            structureRepresentations: sv.structureRepresentations
+        }))
+    }
+
+    componentWillReceiveProps(nextProps: StructureViewComponentProps) {
+        if (nextProps.structureView !== this.props.structureView) {
+            this.setState(this.stateFromStructureView(nextProps.structureView))
+
+            nextProps.structureView.updated.subscribe(() => this.setState({
+                symmetryFeatureIds: nextProps.structureView.getSymmetryFeatureIds(),
+                structureRepresentations: nextProps.structureView.structureRepresentations
+            }))
+        }
+    }
+
+    async update(state: Partial<StructureViewComponentState>) {
+        const sv = this.state.structureView
+
+        if (state.modelId !== undefined) await sv.setModel(state.modelId)
+        if (state.assemblyId !== undefined) await sv.setAssembly(state.assemblyId)
+        if (state.symmetryFeatureId !== undefined) await sv.setSymmetryFeature(state.symmetryFeatureId)
+
+        this.setState(this.stateFromStructureView(sv))
+    }
+
+    render() {
+        const { structureView, label, modelIds, assemblyIds, symmetryFeatureIds, active, structureRepresentations } = this.state
+
+        const modelIdOptions = modelIds.map(m => {
+            return <option key={m.id} value={m.id}>{m.label}</option>
+        })
+        const assemblyIdOptions = assemblyIds.map(a => {
+            return <option key={a.id} value={a.id}>{a.label}</option>
+        })
+        const symmetryFeatureIdOptions = symmetryFeatureIds.map(f => {
+            return <option key={f.id} value={f.id}>{f.label}</option>
+        })
+
+        return <div>
+            <div>
+                <h2>{label}</h2>
+            </div>
+            <div>
+                <div>
+                    <span>Model </span>
+                    <select
+                        style={{width: '100px'}}
+                        value={this.state.modelId}
+                        onChange={(e) => {
+                            this.update({ modelId: parseInt(e.target.value) })
+                        }}
+                    >
+                        {modelIdOptions}
+                    </select>
+                    <span> </span>
+                    <input type='range'
+                        value={this.state.modelId}
+                        min={Math.min(...modelIds.map(m => m.id))}
+                        max={Math.max(...modelIds.map(m => m.id))}
+                        step='1'
+                        onInput={(e) => {
+                            this.update({ modelId: parseInt(e.currentTarget.value) })
+                        }}
+                    >
+                    </input>
+                </div>
+                <div>
+                    <span>Assembly </span>
+                    <select
+                        style={{width: '150px'}}
+                        value={this.state.assemblyId}
+                        onChange={(e) => {
+                            this.update({ assemblyId: e.target.value })
+                        }}
+                    >
+                        {assemblyIdOptions}
+                    </select>
+                </div>
+                <div>
+                    <span>Symmetry Feature </span>
+                    <select
+                        style={{width: '150px'}}
+                        value={this.state.symmetryFeatureId}
+                        onChange={(e) => {
+                            this.update({ symmetryFeatureId: parseInt(e.target.value) })
+                        }}
+                    >
+                        {symmetryFeatureIdOptions}
+                    </select>
+                </div>
+                <div>
+                    <h4>Active</h4>
+                    { Object.keys(active).map((k, i) => {
+                        return <div key={i}>
+                            <input
+                                type='checkbox'
+                                checked={active[k]}
+                                onChange={(e) => {
+                                    const sv = structureView
+                                    if (k === 'symmetryAxes') {
+                                        sv.setSymmetryAxes(e.target.checked)
+                                    } else if (Object.keys(sv.structureRepresentations).includes(k)) {
+                                        sv.setStructureRepresentation(k, e.target.checked)
+                                    }
+                                }}
+                            /> {k}
+                        </div>
+                    } ) }
+                </div>
+                <div>
+                    <h3>Structure Representations</h3>
+                    { Object.keys(structureRepresentations).map((k, i) => {
+                        if (active[k]) {
+                            return <div key={i}>
+                                <StructureRepresentationComponent
+                                    representation={structureRepresentations[k]}
+                                    viewer={structureView.viewer}
+                                />
+                            </div>
+                        } else {
+                            return ''
+                        }
+                    } ) }
+                </div>
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..90ec862422d69ad86960d61fb6ebf8bce59fc44b
--- /dev/null
+++ b/src/apps/canvas/component/viewport.tsx
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { App } from '../app';
+import { MarkerAction } from 'mol-geo/util/marker-data';
+import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci';
+import { labelFirst } from 'mol-view/label';
+
+interface ViewportProps {
+    app: App
+}
+
+interface ViewportState {
+    noWebGl: boolean,
+    info: string
+}
+
+export class Viewport extends React.Component<ViewportProps, ViewportState> {
+    private container: HTMLDivElement | null = null;
+    private canvas: HTMLCanvasElement | null = null;
+
+    state: ViewportState = {
+        noWebGl: false,
+        info: ''
+    };
+
+    handleResize() {
+        this.props.app.viewer.handleResize()
+    }
+
+    componentDidMount() {
+        if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) {
+            this.setState({ noWebGl: true });
+        }
+        this.handleResize()
+
+        const viewer = this.props.app.viewer
+
+        viewer.input.resize.subscribe(() => this.handleResize())
+
+        let prevLoci: Loci = EmptyLoci
+        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) return
+            const p = viewer.identify(x, y)
+            if (p) {
+                const loci = viewer.getLoci(p)
+
+                if (!areLociEqual(loci, prevLoci)) {
+                    viewer.mark(prevLoci, MarkerAction.RemoveHighlight)
+                    viewer.mark(loci, MarkerAction.Highlight)
+                    prevLoci = loci
+
+                    const label = labelFirst(loci)
+                    const info = `${label}`
+                    this.setState({ info })
+                }
+            }
+        })
+    }
+
+    componentWillUnmount() {
+        if (super.componentWillUnmount) super.componentWillUnmount();
+        // TODO viewer cleanup
+    }
+
+    renderMissing() {
+        return <div>
+            <div>
+                <p><b>WebGL does not seem to be available.</b></p>
+                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p>
+                <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
+            </div>
+        </div>
+    }
+
+    render() {
+        if (this.state.noWebGl) return this.renderMissing();
+
+        return <div style={{ backgroundColor: 'rgb(0, 0, 0)', width: '100%', height: '100%'}}>
+            <div ref={elm => this.container = elm} style={{width: '100%', height: '100%'}}>
+                <canvas ref={elm => this.canvas = elm}></canvas>
+            </div>
+            <div
+                style={{
+                    position: 'absolute',
+                    top: 10,
+                    left: 10,
+                    padding: 10,
+                    color: 'lightgrey',
+                    background: 'rgba(0, 0, 0, 0.2)'
+                }}
+            >
+                {this.state.info}
+            </div>
+        </div>;
+    }
+}
\ No newline at end of file
diff --git a/src/apps/canvas/index.html b/src/apps/canvas/index.html
index 3aa50333eec315dc64f79dce7366971dda63cc71..bc9f73aa6dfcb30b9a82e79ed5fa3ce5cdf07e24 100644
--- a/src/apps/canvas/index.html
+++ b/src/apps/canvas/index.html
@@ -4,12 +4,30 @@
         <meta charset="utf-8" />
         <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
         <title>Mol* Canvas</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+            }
+            html, body {
+                width: 100%;
+                height: 100%;
+                overflow: hidden;
+            }
+            hr {
+                margin: 10px;
+            }
+            h1, h2, h3, h4, h5 {
+                margin-top: 5px;
+                margin-bottom: 3px;
+            }
+            button {
+                padding: 2px;
+            }
+        </style>
     </head>
     <body>
-        <div id="container" style="width:800px; height: 600px;">
-            <canvas id="canvas"></canvas>
-        </div>
-        <span id="info"></span>
+        <div id="app" style="width: 100%; height: 100%"></div>
         <script type="text/javascript" src="./index.js"></script>
     </body>
 </html>
\ No newline at end of file
diff --git a/src/apps/canvas/index.ts b/src/apps/canvas/index.ts
index 5166630cac8f8059edde039026cabcaaf9bade2c..0c110a8856d8a28fa1a44b277c49c3727cab9222 100644
--- a/src/apps/canvas/index.ts
+++ b/src/apps/canvas/index.ts
@@ -4,151 +4,21 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import './index.html'
-
-import Viewer from 'mol-view/viewer';
-import CIF, { CifBlock } from 'mol-io/reader/cif'
-// import { parse as parseObj } from 'mol-io/reader/obj/parser'
-import { readUrlAs } from 'mol-util/read'
-import { Model, Format, Structure, StructureSymmetry } from 'mol-model/structure';
-import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon';
-import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
-import { EveryLoci } from 'mol-model/loci';
-import { MarkerAction } from 'mol-geo/util/marker-data';
-import { labelFirst } from 'mol-view/label';
-import { Queries as Q, StructureProperties as SP, StructureSelection, StructureQuery } from 'mol-model/structure';
-import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
-import { ShapeRepresentation } from 'mol-geo/representation/shape';
-import { Vec3, Mat4 } from 'mol-math/linear-algebra';
-import { Shape } from 'mol-model/shape';
-import { Color } from 'mol-util/color';
-import { addSphere } from 'mol-geo/mesh/builder/sphere';
-import { Box } from 'mol-geo/primitive/box';
-
-const container = document.getElementById('container')
-if (!container) throw new Error('Can not find element with id "container".')
-
-const canvas = document.getElementById('canvas') as HTMLCanvasElement
-if (!canvas) throw new Error('Can not find element with id "canvas".')
-
-const info = document.getElementById('info') as HTMLCanvasElement
-if (!info) throw new Error('Can not find element with id "info".')
-
-const viewer = Viewer.create(canvas, container)
-viewer.animate()
-
-viewer.input.resize.subscribe(() => {
-    // do whatever appropriate
-})
-
-viewer.input.move.subscribe(({x, y, inside, buttons}) => {
-    if (!inside || buttons) return
-    const p = viewer.identify(x, y)
-    const loci = viewer.getLoci(p)
-
-    viewer.mark(EveryLoci, MarkerAction.RemoveHighlight)
-    viewer.mark(loci, MarkerAction.Highlight)
-
-    const label = labelFirst(loci)
-    info.innerText = `${label}`
-})
-
+import * as React from 'react'
+import * as ReactDOM from 'react-dom'
 
-// async function getObjFromUrl(url: string) {
-//     const data = await readUrlAs(url, false) as string
-//     const comp = parseObj(data)
-//     const parsed = await comp.run()
-//     if (parsed.isError) throw parsed
-//     return parsed.result
-// }
-
-async function getCifFromUrl(url: string) {
-    const data = await readUrlAs(url, false)
-    const comp = CIF.parse(data)
-    const parsed = await comp.run()
-    if (parsed.isError) throw parsed
-    return parsed.result.blocks[0]
-}
-
-async function getModelFromMmcif(cif: CifBlock) {
-    const models = await Model.create(Format.mmCIF(cif)).run()
-    return models[0]
-}
-
-async function getStructureFromModel(model: Model, assembly = '1') {
-    const assemblies = model.symmetry.assemblies
-    if (assemblies.length) {
-        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
-    } else {
-        return Structure.ofModel(model)
-    }
-}
-
-async function init() {
-    const cif = await getCifFromUrl('https://files.rcsb.org/download/1crn.cif')
-    const model = await getModelFromMmcif(cif)
-    const structure = await getStructureFromModel(model)
-
-    viewer.center(structure.boundary.sphere.center)
-
-    // cartoon for whole structure
-    const cartoonRepr = CartoonRepresentation()
-    await cartoonRepr.create(structure, {
-        colorTheme: { name: 'chain-id' },
-        sizeTheme: { name: 'uniform', value: 0.2 },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(cartoonRepr)
-
-    // create new structure via query
-    const q1 = Q.generators.atoms({
-        residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7
-    });
-    const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure));
-
-    // ball+stick for new structure
-    const ballStickRepr = BallAndStickRepresentation()
-    await ballStickRepr.create(newStructure, {
-        colorTheme: { name: 'element-symbol' },
-        sizeTheme: { name: 'uniform', value: 0.1 },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(ballStickRepr)
-
-    // create a mesh
-    const meshBuilder = MeshBuilder.create(256, 128)
-    const colors: Color[] = []
-    const labels: string[] = []
-    // red sphere
-    meshBuilder.setGroup(0)
-    colors[0] = Color(0xFF2233)
-    labels[0] = 'red sphere'
-    addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2)
-    // green cube
-    meshBuilder.setGroup(1)
-    colors[1] = Color(0x2233FF)
-    labels[1] = 'blue cube'
-    const t = Mat4.identity()
-    Mat4.fromTranslation(t, Vec3.create(10, 0, 0))
-    Mat4.scale(t, t, Vec3.create(3, 3, 3))
-    meshBuilder.add(t, Box())
-    const mesh = meshBuilder.getMesh()
-    // const mesh = getObjFromUrl('mesh.obj')
+import './index.html'
 
-    // create shape from mesh
-    const shape = Shape.create('myShape', mesh, colors, labels)
+import { App } from './app';
+import { AppComponent } from './component/app';
+import { urlQueryParameter } from 'mol-util/url-query';
 
-    // add representation from shape
-    const customRepr = ShapeRepresentation()
-    await customRepr.create(shape, {
-        colorTheme: { name: 'shape-group' },
-        // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
-        useFog: false // TODO fog not working properly
-    }).run()
-    viewer.add(customRepr)
+const elm = document.getElementById('app') as HTMLElement
+if (!elm) throw new Error('Can not find element with id "app".')
 
-    // ensure the added representations get rendered, i.e. without mouse input
-    viewer.requestDraw()
-}
+const app = new App()
+ReactDOM.render(React.createElement(AppComponent, { app }), elm);
 
-init()
\ No newline at end of file
+const assemblyId = urlQueryParameter('assembly')
+const pdbId = urlQueryParameter('pdb')
+if (pdbId) app.loadPdbId(pdbId, assemblyId)
\ No newline at end of file
diff --git a/src/apps/canvas/structure-view.ts b/src/apps/canvas/structure-view.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b381ab2f082e2918092e7e695c1de8b82a8ffd45
--- /dev/null
+++ b/src/apps/canvas/structure-view.ts
@@ -0,0 +1,371 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Model, Structure } from 'mol-model/structure';
+import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon';
+import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick';
+import { getStructureFromModel } from './util';
+import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
+import { ShapeRepresentation, ShapeProps } from 'mol-geo/representation/shape';
+import { getAxesShape } from './assembly-symmetry';
+import Viewer from 'mol-view/viewer';
+import { CarbohydrateRepresentation } from 'mol-geo/representation/structure/representation/carbohydrate';
+// import { MeshBuilder } from 'mol-geo/mesh/mesh-builder';
+// import { addSphere } from 'mol-geo/mesh/builder/sphere';
+// import { Shape } from 'mol-model/shape';
+// import { Color } from 'mol-util/color';
+// import { computeUnitBoundary } from 'mol-model/structure/structure/util/boundary';
+// import { addBoundingBox } from 'mol-geo/mesh/builder/bounding-box';
+import { PointRepresentation } from 'mol-geo/representation/structure/representation/point';
+import { StructureRepresentation } from 'mol-geo/representation/structure';
+import { BehaviorSubject } from 'rxjs';
+import { SpacefillRepresentation } from 'mol-geo/representation/structure/representation/spacefill';
+import { DistanceRestraintRepresentation } from 'mol-geo/representation/structure/representation/distance-restraint';
+
+export interface StructureView {
+    readonly viewer: Viewer
+
+    readonly label: string
+    readonly models: ReadonlyArray<Model>
+    readonly structure: Structure | undefined
+    readonly assemblySymmetry: AssemblySymmetry | undefined
+
+    readonly active: { [k: string]: boolean }
+    readonly structureRepresentations: { [k: string]: StructureRepresentation<any> }
+    readonly updated: BehaviorSubject<null>
+    readonly symmetryAxes: ShapeRepresentation<ShapeProps>
+
+    setSymmetryAxes(value: boolean): void
+    setStructureRepresentation(name: string, value: boolean): void
+
+    readonly modelId: number
+    readonly assemblyId: string
+    readonly symmetryFeatureId: number
+
+    setModel(modelId: number): Promise<void>
+    getModelIds(): { id: number, label: string }[]
+    setAssembly(assemblyId: string): Promise<void>
+    getAssemblyIds(): { id: string, label: string }[]
+    setSymmetryFeature(symmetryFeatureId: number): Promise<void>
+    getSymmetryFeatureIds(): { id: number, label: string }[]
+
+    destroy: () => void
+}
+
+interface StructureViewProps {
+    assemblyId?: string
+    symmetryFeatureId?: number
+}
+
+
+
+export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> {
+    const active: { [k: string]: boolean } = {
+        cartoon: true,
+        point: false,
+        ballAndStick: false,
+        carbohydrate: false,
+        spacefill: false,
+        distanceRestraint: false,
+        symmetryAxes: false,
+        // polymerSphere: false,
+    }
+
+    const structureRepresentations: { [k: string]: StructureRepresentation<any> } = {
+        cartoon: CartoonRepresentation(),
+        point: PointRepresentation(),
+        ballAndStick: BallAndStickRepresentation(),
+        carbohydrate: CarbohydrateRepresentation(),
+        spacefill: SpacefillRepresentation(),
+        distanceRestraint: DistanceRestraintRepresentation(),
+    }
+
+    const symmetryAxes = ShapeRepresentation()
+    const polymerSphere = ShapeRepresentation()
+
+    const updated: BehaviorSubject<null> = new BehaviorSubject<null>(null)
+
+    let label: string
+    let model: Model | undefined
+    let assemblySymmetry: AssemblySymmetry | undefined
+    let structure: Structure | undefined
+
+    let modelId: number
+    let assemblyId: string
+    let symmetryFeatureId: number
+
+    async function setSymmetryAxes(value: boolean) {
+        if (!value) {
+            assemblySymmetry = undefined
+        } else {
+            await AssemblySymmetry.attachFromCifOrAPI(models[modelId])
+            assemblySymmetry = AssemblySymmetry.get(models[modelId])
+        }
+        active.symmetryAxes = value
+        await setSymmetryFeature()
+    }
+
+    async function setStructureRepresentation(k: string, value: boolean) {
+        active[k] = value
+        await createStructureRepr()
+    }
+
+    async function setModel(newModelId: number, newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        console.log('setModel', newModelId)
+        modelId = newModelId
+        model = models[modelId]
+        if (active.symmetryAxes) {
+            await AssemblySymmetry.attachFromCifOrAPI(model)
+            assemblySymmetry = AssemblySymmetry.get(model)
+        }
+        await setAssembly(newAssemblyId, newSymmetryFeatureId)
+    }
+
+    function getModelIds() {
+        const modelIds: { id: number, label: string }[] = []
+        models.forEach((m, i) => {
+            modelIds.push({ id: i, label: `${i}: ${m.label} #${m.modelNum}` })
+        })
+        return modelIds
+    }
+
+    async function setAssembly(newAssemblyId?: string, newSymmetryFeatureId?: number) {
+        console.log('setAssembly', newAssemblyId)
+        if (newAssemblyId !== undefined) {
+            assemblyId = newAssemblyId
+        } else if (model && model.symmetry.assemblies.length) {
+            assemblyId = model.symmetry.assemblies[0].id
+        } else if (model) {
+            assemblyId = '0'
+        } else {
+            assemblyId = '-1'
+        }
+        await getStructure()
+        await setSymmetryFeature(newSymmetryFeatureId)
+    }
+
+    function getAssemblyIds() {
+        const assemblyIds: { id: string, label: string }[] = [
+            { id: '0', label: '0: model' }
+        ]
+        if (model) model.symmetry.assemblies.forEach(a => {
+            assemblyIds.push({ id: a.id, label: `${a.id}: ${a.details}` })
+        })
+        return assemblyIds
+    }
+
+    async function setSymmetryFeature(newSymmetryFeatureId?: number) {
+        console.log('setSymmetryFeature', newSymmetryFeatureId)
+        if (newSymmetryFeatureId !== undefined) {
+            symmetryFeatureId = newSymmetryFeatureId
+        } else if (assemblySymmetry) {
+            const f = assemblySymmetry.getFeatures(assemblyId)
+            if (f._rowCount) {
+                symmetryFeatureId = f.id.value(0)
+            } else {
+                symmetryFeatureId = -1
+            }
+        } else {
+            symmetryFeatureId = -1
+        }
+        await createSymmetryRepr()
+    }
+
+    function getSymmetryFeatureIds() {
+        const symmetryFeatureIds: { id: number, label: string }[] = []
+        if (assemblySymmetry) {
+            const symmetryFeatures = assemblySymmetry.getFeatures(assemblyId)
+            for (let i = 0, il = symmetryFeatures._rowCount; i < il; ++i) {
+                const id = symmetryFeatures.id.value(i)
+                const symmetry = symmetryFeatures.symmetry_value.value(i)
+                const type = symmetryFeatures.type.value(i)
+                const stoichiometry = symmetryFeatures.stoichiometry_value.value(i)
+                const label = `${id}: ${symmetry} ${type} ${stoichiometry}`
+                symmetryFeatureIds.push({ id, label })
+            }
+        }
+        return symmetryFeatureIds
+    }
+
+    async function getStructure() {
+        if (model) structure = await getStructureFromModel(model, assemblyId)
+        if (model && structure) {
+            label = `${model.label} - Assembly ${assemblyId}`
+        } else {
+            label = ''
+        }
+        await createStructureRepr()
+    }
+
+    async function createStructureRepr() {
+        if (structure) {
+            console.log('createStructureRepr')
+            for (const k in structureRepresentations) {
+                if (active[k]) {
+                    await structureRepresentations[k].createOrUpdate({}, structure).run()
+                    viewer.add(structureRepresentations[k])
+                } else {
+                    viewer.remove(structureRepresentations[k])
+                }
+            }
+
+            viewer.center(structure.boundary.sphere.center)
+
+            // const mb = MeshBuilder.create()
+            // mb.setGroup(0)
+            // addSphere(mb, structure.boundary.sphere.center, structure.boundary.sphere.radius, 3)
+            // addBoundingBox(mb, structure.boundary.box, 1, 2, 8)
+            // for (let i = 0, il = structure.units.length; i < il; ++i) {
+            //     mb.setGroup(1)
+            //     const u = structure.units[i]
+            //     const ci = u.model.atomicHierarchy.chainAtomSegments.index[u.elements[0]]
+            //     const ek = u.model.atomicHierarchy.getEntityKey(ci)
+            //     if (u.model.entities.data.type.value(ek) === 'water') continue
+            //     const boundary = computeUnitBoundary(u)
+            //     addSphere(mb, boundary.sphere.center, boundary.sphere.radius, 3)
+            //     addBoundingBox(mb, boundary.box, 0.5, 2, 8)
+            // }
+            // const shape = Shape.create('boundary', mb.getMesh(), [Color(0xCC6633), Color(0x3366CC)], ['sphere boundary'])
+            // await polymerSphere.createOrUpdate({
+            //     alpha: 0.5,
+            //     doubleSided: false,
+            //     depthMask: false,
+            //     useFog: false // TODO fog not working properly
+            // }, shape).run()
+        } else {
+            for (const k in structureRepresentations) structureRepresentations[k].destroy()
+            polymerSphere.destroy()
+        }
+
+        viewer.add(polymerSphere)
+
+        updated.next(null)
+        viewer.requestDraw(true)
+        console.log(viewer.stats)
+    }
+
+    async function createSymmetryRepr() {
+        if (assemblySymmetry) {
+            const features = assemblySymmetry.getFeatures(assemblyId)
+            if (features._rowCount) {
+                const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry)
+                if (axesShape) {
+                    // const colorTheme = getClusterColorTheme(symmetryFeatureId, assemblySymmetry)
+                    // await cartoon.createOrUpdate({
+                    //     colorTheme: { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity },
+                    //     sizeTheme: { name: 'uniform', value: 0.2 },
+                    //     useFog: false // TODO fog not working properly
+                    // }).run()
+                    // await ballAndStick.createOrUpdate({
+                    //     colorTheme:  { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity },
+                    //     sizeTheme: { name: 'uniform', value: 0.1 },
+                    //     useFog: false // TODO fog not working properly
+                    // }).run()
+                    await symmetryAxes.createOrUpdate({}, axesShape).run()
+                    viewer.add(symmetryAxes)
+                } else {
+                    viewer.remove(symmetryAxes)
+                }
+            } else {
+                viewer.remove(symmetryAxes)
+            }
+        } else {
+            viewer.remove(symmetryAxes)
+        }
+        updated.next(null)
+        viewer.requestDraw(true)
+    }
+
+    await setModel(0, props.assemblyId, props.symmetryFeatureId)
+
+    return {
+        viewer,
+
+        get label() { return label },
+        models,
+        get structure() { return structure },
+        get assemblySymmetry() { return assemblySymmetry },
+
+        active,
+        structureRepresentations,
+        updated,
+        symmetryAxes,
+
+        setSymmetryAxes,
+        setStructureRepresentation,
+
+        get modelId() { return modelId },
+        get assemblyId() { return assemblyId },
+        get symmetryFeatureId() { return symmetryFeatureId },
+
+        setModel,
+        getModelIds,
+        setAssembly,
+        getAssemblyIds,
+        setSymmetryFeature,
+        getSymmetryFeatureIds,
+
+        destroy: () => {
+            for (const k in structureRepresentations) {
+                viewer.remove(structureRepresentations[k])
+                structureRepresentations[k].destroy()
+            }
+            viewer.remove(polymerSphere)
+            viewer.remove(symmetryAxes)
+            viewer.requestDraw(true)
+
+            polymerSphere.destroy()
+            symmetryAxes.destroy()
+        }
+    }
+}
+
+// // create new structure via query
+// const q1 = Q.generators.atoms({
+//     residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7
+// });
+// const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure));
+
+// // ball+stick for new structure
+// const newBallStickRepr = BallAndStickRepresentation()
+// await newBallStickRepr.create(newStructure, {
+//     colorTheme: { name: 'element-symbol' },
+//     sizeTheme: { name: 'uniform', value: 0.1 },
+//     useFog: false // TODO fog not working properly
+// }).run()
+// viewer.add(newBallStickRepr)
+
+// // create a mesh
+// const meshBuilder = MeshBuilder.create(256, 128)
+// const colors: Color[] = []
+// const labels: string[] = []
+// // red sphere
+// meshBuilder.setGroup(0)
+// colors[0] = Color(0xFF2233)
+// labels[0] = 'red sphere'
+// addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2)
+// // green cube
+// meshBuilder.setGroup(1)
+// colors[1] = Color(0x2233FF)
+// labels[1] = 'blue cube'
+// const t = Mat4.identity()
+// Mat4.fromTranslation(t, Vec3.create(10, 0, 0))
+// Mat4.scale(t, t, Vec3.create(3, 3, 3))
+// meshBuilder.add(t, Box())
+// const mesh = meshBuilder.getMesh()
+// const mesh = getObjFromUrl('mesh.obj')
+
+// // create shape from mesh
+// const shape = Shape.create('myShape', mesh, colors, labels)
+
+// // add representation from shape
+// const customRepr = ShapeRepresentation()
+// await customRepr.create(shape, {
+//     colorTheme: { name: 'shape-group' },
+//     // colorTheme: { name: 'uniform', value: Color(0xFFCC22) },
+//     useFog: false // TODO fog not working properly
+// }).run()
+// viewer.add(customRepr)
\ No newline at end of file
diff --git a/src/apps/canvas/util.ts b/src/apps/canvas/util.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9a2c8f7fa9f6d9b27d5f4973450962c741a7a7ba
--- /dev/null
+++ b/src/apps/canvas/util.ts
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import CIF, { CifBlock } from 'mol-io/reader/cif'
+import { readUrlAs, readFileAs } from 'mol-util/read';
+import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure';
+// import { parse as parseObj } from 'mol-io/reader/obj/parser'
+
+// export async function getObjFromUrl(url: string) {
+//     const data = await readUrlAs(url, false) as string
+//     const comp = parseObj(data)
+//     const parsed = await comp.run()
+//     if (parsed.isError) throw parsed
+//     return parsed.result
+// }
+
+export async function getCifFromData(data: string | Uint8Array) {
+    const comp = CIF.parse(data)
+    const parsed = await comp.run()
+    if (parsed.isError) throw parsed
+    return parsed.result.blocks[0]
+}
+
+export async function getCifFromUrl(url: string) {
+    return getCifFromData(await readUrlAs(url, false))
+}
+
+export async function getCifFromFile(file: File, binary = false) {
+    return getCifFromData(await readFileAs(file, binary))
+}
+
+export async function getModelsFromMmcif(cif: CifBlock) {
+    return await Model.create(Format.mmCIF(cif)).run()
+}
+
+export async function getStructureFromModel(model: Model, assembly: string) {
+    const assemblies = model.symmetry.assemblies
+    if (assembly === '0') {
+        return Structure.ofModel(model)
+    } else if (assemblies.find(a => a.id === assembly)) {
+        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
+    }
+}
\ No newline at end of file
diff --git a/src/apps/viewer/index.tsx b/src/apps/viewer/index.tsx
index 940efc7d074991890d466e3ec52e10da2fa25c69..f1c68970f4779c1fd3a8740d974f277b3c464298 100644
--- a/src/apps/viewer/index.tsx
+++ b/src/apps/viewer/index.tsx
@@ -104,10 +104,4 @@ ctx.dispatcher.getStream(InteractivityEvents.HighlightLoci).subscribe(event => {
     }
 })
 
-ctx.dispatcher.getStream(InteractivityEvents.SelectLoci).subscribe(event => {
-    if (event && event.data) {
-        ctx.stage.viewer.mark(event.data, MarkerAction.ToggleSelect)
-    }
-})
-
 ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm);
diff --git a/src/mol-app/ui/visualization/viewport.tsx b/src/mol-app/ui/visualization/viewport.tsx
index d23f182e1b5bbfa3731d1f98edc7fa2cb9243692..ed6c563a40673701268de1330556a64680dc23b9 100644
--- a/src/mol-app/ui/visualization/viewport.tsx
+++ b/src/mol-app/ui/visualization/viewport.tsx
@@ -163,19 +163,24 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl?
         viewer.input.move.subscribe(({x, y, inside, buttons}) => {
             if (!inside || buttons) return
             const p = viewer.identify(x, y)
-            const loci = viewer.getLoci(p)
-            InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci);
-
-            // TODO use LabelLoci event and make configurable
-            const label = labelFirst(loci)
-            const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}`
-            this.setState({ info })
+            if (p) {
+                const loci = viewer.getLoci(p)
+                InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci);
+
+                // TODO use LabelLoci event and make configurable
+                const label = labelFirst(loci)
+                const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}`
+                this.setState({ info })
+            }
         })
 
         // TODO filter only for left button/single finger touch?
         viewer.input.click.subscribe(({x, y}) => {
-            const loci = viewer.getLoci(viewer.identify(x, y))
-            InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci);
+            const p = viewer.identify(x, y)
+            if (p) {
+                const loci = viewer.getLoci(p)
+                InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci);
+            }
         })
     }
 
diff --git a/src/mol-data/db/table.ts b/src/mol-data/db/table.ts
index 15fd6aef229002f97e13fde5f609131cb19fa245..3573c7bd604a7ce5bf5877efbd1a53a89d38bcec 100644
--- a/src/mol-data/db/table.ts
+++ b/src/mol-data/db/table.ts
@@ -101,6 +101,14 @@ namespace Table {
         return ret as Table<R>;
     }
 
+    export function pick<S extends R, R extends Schema>(table: Table<S>, schema: R, test: (i: number) => boolean) {
+        const _view: number[] = []
+        for (let i = 0, il = table._rowCount; i < il; ++i) {
+            if (test(i)) _view.push(i)
+        }
+        return view(table, schema, _view)
+    }
+
     export function window<S extends R, R extends Schema>(table: Table<S>, schema: R, start: number, end: number) {
         if (start === 0 && end === table._rowCount) return table;
         const ret = Object.create(null);
@@ -194,6 +202,13 @@ namespace Table {
         return row;
     }
 
+    /** Pick the first row for which `test` evaluates to true */
+    export function pickRow<S extends Schema>(table: Table<S>, test: (i: number) => boolean) {
+        for (let i = 0, il = table._rowCount; i < il; ++i) {
+            if (test(i)) return getRow(table, i)
+        }
+    }
+
     export function getRows<S extends Schema>(table: Table<S>) {
         const ret: Row<S>[] = [];
         const { _rowCount: c } = table;
diff --git a/src/mol-data/int/impl/interval.ts b/src/mol-data/int/impl/interval.ts
index f7568480900db112b77c934e7e4d856a923062b8..fe293e0a0c013897a68b07321349a5e8277ee6e9 100644
--- a/src/mol-data/int/impl/interval.ts
+++ b/src/mol-data/int/impl/interval.ts
@@ -19,6 +19,7 @@ export function size(i: Tuple) { return Tuple.snd(i) - Tuple.fst(i); }
 export const hashCode = Tuple.hashCode;
 
 export function has(int: Tuple, v: number) { return Tuple.fst(int) <= v && v < Tuple.snd(int); }
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(int: Tuple, x: number) { const m = start(int); return x >= m && x < end(int) ? x - m : -1; }
 export function getAt(int: Tuple, i: number) { return Tuple.fst(int) + i; }
 
diff --git a/src/mol-data/int/impl/ordered-set.ts b/src/mol-data/int/impl/ordered-set.ts
index 27ee1f1db798f09ebc789a2043520656c63ebec3..33794046c652e26bba6469695c510d928ad46977 100644
--- a/src/mol-data/int/impl/ordered-set.ts
+++ b/src/mol-data/int/impl/ordered-set.ts
@@ -25,6 +25,7 @@ export function ofSortedArray(xs: Nums): OrderedSetImpl {
 
 export function size(set: OrderedSetImpl) { return I.is(set) ? I.size(set) : S.size(set); }
 export function has(set: OrderedSetImpl, x: number) { return I.is(set) ? I.has(set, x) : S.has(set, x); }
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(set: OrderedSetImpl, x: number) { return I.is(set) ? I.indexOf(set, x) : S.indexOf(set, x); }
 export function getAt(set: OrderedSetImpl, i: number) { return I.is(set) ? I.getAt(set, i) : set[i]; }
 export function min(set: OrderedSetImpl) { return I.is(set) ? I.min(set) : S.min(set); }
diff --git a/src/mol-data/int/impl/sorted-array.ts b/src/mol-data/int/impl/sorted-array.ts
index 2d441bc3fdf4304e7808e9a417d64a98060441a6..6fff1aa007c47f26cc99d19e8ed790e0e57c4398 100644
--- a/src/mol-data/int/impl/sorted-array.ts
+++ b/src/mol-data/int/impl/sorted-array.ts
@@ -36,6 +36,7 @@ export function hashCode(xs: Nums) {
     return hash3(s, xs[0], xs[s - 1]);
 }
 
+/** Returns the index of `x` in `set` or -1 if not found. */
 export function indexOf(xs: Nums, v: number) {
     const l = xs.length;
     return l === 0 ? -1 : xs[0] <= v && v <= xs[l - 1] ? binarySearchRange(xs, v, 0, l) : -1;
diff --git a/src/mol-data/int/interval.ts b/src/mol-data/int/interval.ts
index b2ab7fb8350624c57d88f4d39960e6f41465abe7..ef72450b05865a03ecbffc4562e06c81ada34f49 100644
--- a/src/mol-data/int/interval.ts
+++ b/src/mol-data/int/interval.ts
@@ -18,6 +18,7 @@ namespace Interval {
 
     /** Test if a value is within the bounds of the interval */
     export const has: <T extends number = number>(interval: Interval<T>, x: T) => boolean = Impl.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(interval: Interval<T>, x: T) => number = Impl.indexOf as any;
     export const getAt: <T extends number = number>(interval: Interval<T>, i: number) => T = Impl.getAt as any;
 
diff --git a/src/mol-data/int/ordered-set.ts b/src/mol-data/int/ordered-set.ts
index d18e61446d77582a7c5e8d6af78c586db838f875..046c1b6ab94b3acf93b5b5d232eb711c8b054402 100644
--- a/src/mol-data/int/ordered-set.ts
+++ b/src/mol-data/int/ordered-set.ts
@@ -18,6 +18,7 @@ namespace OrderedSet {
     export const ofSortedArray: <T extends number = number>(xs: ArrayLike<T>) => OrderedSet<T> = Base.ofSortedArray as any;
 
     export const has: <T extends number = number>(set: OrderedSet<T>, x: T) => boolean = Base.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(set: OrderedSet<T>, x: T) => number = Base.indexOf as any;
     export const getAt: <T extends number = number>(set: OrderedSet<T>, i: number) => T = Base.getAt as any;
 
diff --git a/src/mol-data/int/sorted-array.ts b/src/mol-data/int/sorted-array.ts
index c0f1248db3f25e3c7764c67164412d1baafb0c7a..cf0f5d11e09470a4b520cb00707faf4e7f034ea3 100644
--- a/src/mol-data/int/sorted-array.ts
+++ b/src/mol-data/int/sorted-array.ts
@@ -19,6 +19,7 @@ namespace SortedArray {
     export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
 
     export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
+    /** Returns the index of `x` in `set` or -1 if not found. */
     export const indexOf: <T extends number = number>(array: SortedArray<T>, x: T) => number = Impl.indexOf as any;
     export const indexOfInInterval: <T extends number = number>(array: SortedArray<T>, x: number, bounds: Interval) => number = Impl.indexOfInInterval as any;
 
diff --git a/src/mol-data/int/sorted-ranges.ts b/src/mol-data/int/sorted-ranges.ts
index d51ba633178667806c81f5e775878acbfa588e03..967595e0f670ba6840665ca03ebce519fcdde4bb 100644
--- a/src/mol-data/int/sorted-ranges.ts
+++ b/src/mol-data/int/sorted-ranges.ts
@@ -18,7 +18,7 @@ namespace SortedRanges {
     export function max<T extends number = number>(ranges: SortedRanges<T>) { return ranges[ranges.length - 1] }
     export function size<T extends number = number>(ranges: SortedRanges<T>) {
         let size = 0
-        for(let i = 0, il = ranges.length; i < il; i += 2) {
+        for (let i = 0, il = ranges.length; i < il; i += 2) {
             size += ranges[i + 1] - ranges[i] + 1
         }
         return size
diff --git a/src/mol-data/util/hash-functions.ts b/src/mol-data/util/hash-functions.ts
index d822d8d9c4c1f60d77106d1d2b00526d146fc75b..0f672694237f5d4c7c6a6d7c1df15527582ece27 100644
--- a/src/mol-data/util/hash-functions.ts
+++ b/src/mol-data/util/hash-functions.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 // from http://burtleburtle.net/bob/hash/integer.html
@@ -67,4 +68,16 @@ export function cantorPairing(a: number, b: number) {
  */
 export function sortedCantorPairing(a: number, b: number) {
     return a < b ? cantorPairing(a, b) : cantorPairing(b, a);
+}
+
+/**
+ * 32 bit FNV-1a hash, see http://isthe.com/chongo/tech/comp/fnv/
+ */
+export function hashFnv32a(array: number[]) {
+    let hval = 0x811c9dc5;
+    for (let i = 0, il = array.length; i < il; ++i) {
+        hval ^= array[i];
+        hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
+    }
+    return hval >>> 0;
 }
\ No newline at end of file
diff --git a/src/mol-geo/mesh/builder/bounding-box.ts b/src/mol-geo/mesh/builder/bounding-box.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ba36360454d6d689d8b392631543a2be861ff9cb
--- /dev/null
+++ b/src/mol-geo/mesh/builder/bounding-box.ts
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Box3D } from 'mol-math/geometry';
+import { MeshBuilder } from '../mesh-builder';
+import { CylinderProps } from '../../primitive/cylinder';
+import { addCylinder } from './cylinder';
+import { addSphere } from './sphere';
+
+const tmpStart = Vec3.zero()
+const tmpEnd = Vec3.zero()
+const cylinderProps: CylinderProps = {}
+
+export function addBoundingBox(builder: MeshBuilder, box: Box3D, radius: number, detail: number, radialSegments: number) {
+    const { min, max } = box
+
+    cylinderProps.radiusTop = radius
+    cylinderProps.radiusBottom = radius
+    cylinderProps.radialSegments = radialSegments
+
+    Vec3.set(tmpStart, max[0], max[1], max[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], min[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, min[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, max[0], min[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], min[1], max[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, max[0], min[1], max[2])
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+
+    Vec3.set(tmpStart, min[0], max[1], min[2])
+    addSphere(builder, tmpStart, radius, detail)
+    Vec3.set(tmpEnd, max[0], max[1], min[2])
+    addSphere(builder, tmpEnd, radius, detail)
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+    Vec3.set(tmpEnd, min[0], max[1], max[2])
+    addSphere(builder, tmpEnd, radius, detail)
+    addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps)
+}
\ No newline at end of file
diff --git a/src/mol-geo/mesh/builder/tube.ts b/src/mol-geo/mesh/builder/tube.ts
index 944429468a81cbf653b99bc69295b8c5ca012c9a..eb3de091cec07adeed2b354b661be988ff42f32e 100644
--- a/src/mol-geo/mesh/builder/tube.ts
+++ b/src/mol-geo/mesh/builder/tube.ts
@@ -84,7 +84,7 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>,
         Vec3.fromArray(u, normalVectors, offset)
         Vec3.fromArray(v, binormalVectors, offset)
         Vec3.fromArray(controlPoint, controlPoints, offset)
-        Vec3.cross(normalVector, u, v)
+        Vec3.cross(normalVector, v, u)
 
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
@@ -107,9 +107,9 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>,
 
             ChunkedArray.add3(
                 indices,
-                centerVertex,
+                vertexCount + (i + 1) % radialSegments,
                 vertexCount + i,
-                vertexCount + (i + 1) % radialSegments
+                centerVertex
             );
         }
     }
diff --git a/src/mol-geo/representation/index.ts b/src/mol-geo/representation/index.ts
index a444545fe64d4823516e40b01a07e054b43239e9..ce1425040fd53f1705d7e241c155ddb6a2db4d5e 100644
--- a/src/mol-geo/representation/index.ts
+++ b/src/mol-geo/representation/index.ts
@@ -13,20 +13,19 @@ import { MarkerAction } from '../util/marker-data';
 export interface RepresentationProps {}
 
 export interface Representation<D, P extends RepresentationProps = {}> {
+    readonly label: string
     readonly renderObjects: ReadonlyArray<RenderObject>
     readonly props: Readonly<P>
-    create: (data: D, props?: Partial<P>) => Task<void>
-    update: (props: Partial<P>) => Task<void>
+    createOrUpdate: (props?: Partial<P>, data?: D) => Task<void>
     getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => void
+    mark: (loci: Loci, action: MarkerAction) => boolean
     destroy: () => void
 }
 
 export interface Visual<D, P extends RepresentationProps = {}> {
-    readonly renderObject: RenderObject
-    create: (ctx: RuntimeContext, data: D, props?: Partial<P>) => Promise<void>
-    update: (ctx: RuntimeContext, props: Partial<P>) => Promise<boolean>
+    readonly renderObject: RenderObject | undefined
+    createOrUpdate: (ctx: RuntimeContext, props?: Partial<P>, data?: D) => Promise<void>
     getLoci: (pickingId: PickingId) => Loci
-    mark: (loci: Loci, action: MarkerAction) => void
+    mark: (loci: Loci, action: MarkerAction) => boolean
     destroy: () => void
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/shape/index.ts b/src/mol-geo/representation/shape/index.ts
index c9272af8df70789e128b6dc0be8d095f46504462..c5b86178a700bd021cbd759e1dcfe36ee43c5df2 100644
--- a/src/mol-geo/representation/shape/index.ts
+++ b/src/mol-geo/representation/shape/index.ts
@@ -10,7 +10,7 @@ import { RepresentationProps, Representation } from '..';
 import { PickingId } from '../../util/picking';
 import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data';
-import { createRenderableState, createMeshValues, createIdentityTransform, DefaultMeshProps } from '../util';
+import { createRenderableState, createMeshValues, DefaultMeshProps } from '../util';
 import { getMeshData } from '../../util/mesh-data';
 import { MeshValues } from 'mol-gl/renderable';
 import { ValueCell } from 'mol-util';
@@ -19,40 +19,48 @@ import { Shape } from 'mol-model/shape';
 import { LocationIterator } from '../../util/location-iterator';
 import { createColors } from '../structure/visual/util/common';
 import { OrderedSet, Interval } from 'mol-data/int';
+import { createIdentityTransform } from '../../util/transform-data';
 
 export interface ShapeRepresentation<P extends RepresentationProps = {}> extends Representation<Shape, P> { }
 
 export const DefaultShapeProps = {
     ...DefaultMeshProps,
+
     colorTheme: { name: 'shape-group' } as ColorThemeProps
 }
 export type ShapeProps = typeof DefaultShapeProps
 
+// TODO
+// export type ShapeRepresentation = ShapeRepresentation<ShapeProps>
+
 export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation<P> {
     const renderObjects: RenderObject[] = []
-    let _renderObject: MeshRenderObject
+    let _renderObject: MeshRenderObject | undefined
     let _shape: Shape
     let _props: P
 
-    function create(shape: Shape, props: Partial<P> = {}) {
+    function createOrUpdate(props: Partial<P> = {}, shape?: Shape) {
         _props = Object.assign({}, DefaultShapeProps, _props, props)
-        _shape = shape
+        if (shape) _shape = shape
 
         return Task.create('ShapeRepresentation.create', async ctx => {
             renderObjects.length = 0
 
-            const mesh = shape.mesh
-            const locationIt = ShapeGroupIterator.fromShape(shape)
+            if (!_shape) return
+
+            const mesh = _shape.mesh
+            const locationIt = ShapeGroupIterator.fromShape(_shape)
             const { groupCount, instanceCount } = locationIt
 
-            const color = createColors(locationIt, _props.colorTheme)
+            const transform = createIdentityTransform()
+            const color = await createColors(ctx, locationIt, _props.colorTheme)
             const marker = createMarkers(instanceCount * groupCount)
             const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
 
             const values: MeshValues = {
                 ...getMeshData(mesh),
                 ...createMeshValues(_props, counts),
-                aTransform: createIdentityTransform(),
+                ...transform,
                 ...color,
                 ...marker,
 
@@ -61,31 +69,24 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation
             const state = createRenderableState(_props)
 
             _renderObject = createMeshRenderObject(values, state)
-            console.log(_renderObject)
             renderObjects.push(_renderObject)
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('ShapeRepresentation.update', async ctx => {
-            // TODO handle general update
-            // TODO check shape.colors.ref.version
-        })
-    }
-
     return {
+        label: 'Shape mesh',
         get renderObjects () { return renderObjects },
         get props () { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci(pickingId: PickingId) {
             const { objectId, groupId } = pickingId
-            if (_renderObject.id === objectId) {
+            if (_renderObject && _renderObject.id === objectId) {
                 return Shape.Loci([ { shape: _shape, ids: OrderedSet.ofSingleton(groupId) } ])
             }
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!_renderObject) return false
             const { tMarker } = _renderObject.values
             let changed = false
             if (isEveryLoci(loci)) {
@@ -107,9 +108,12 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObjects.length = 0
+            _renderObject = undefined
         }
     }
 }
diff --git a/src/mol-geo/representation/structure/complex-representation.ts b/src/mol-geo/representation/structure/complex-representation.ts
index 6a258b24ca99cf9ccc9edc5d1297b306ac483051..3f624d4af188435dccb9238decbf223f9de2665c 100644
--- a/src/mol-geo/representation/structure/complex-representation.ts
+++ b/src/mol-geo/representation/structure/complex-representation.ts
@@ -8,70 +8,43 @@
 import { Structure } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { PickingId } from '../../util/picking';
-import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
+import { Loci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
-import { getQualityProps } from '../util';
-import { StructureProps, DefaultStructureProps, StructureRepresentation } from '.';
+import { StructureProps, StructureRepresentation } from '.';
 import { ComplexVisual } from './complex-visual';
 
-export function ComplexRepresentation<P extends StructureProps>(visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
-    let visual: ComplexVisual<P>
-
+export function ComplexRepresentation<P extends StructureProps>(label: string, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
+    let visual: ComplexVisual<P> | undefined
     let _props: P
-    let _structure: Structure
 
-    function create(structure: Structure, props: Partial<P> = {}) {
-        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
-        _props.colorTheme.structure = structure
+    function createOrUpdate(props: Partial<P> = {}, structure?: Structure) {
+        _props = Object.assign({}, _props, props)
 
         return Task.create('Creating StructureRepresentation', async ctx => {
-            if (!_structure) {
-                visual = visualCtor()
-                await visual.create(ctx, structure, _props)
-            } else {
-                if (_structure.hashCode === structure.hashCode) {
-                    await update(_props)
-                } else {
-                    if (!await visual.update(ctx, _props)) {
-                        await visual.create(ctx, structure, _props)
-                    }
-                }
-            }
-            _structure = structure
+            if (!visual) visual = visualCtor()
+            await visual.createOrUpdate(ctx, _props, structure)
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('Updating StructureRepresentation', async ctx => {
-            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
-            _props.colorTheme.structure = _structure
-
-            if (!await visual.update(ctx, _props)) {
-                await visual.create(ctx, _structure, _props)
-            }
-        })
-    }
-
     function getLoci(pickingId: PickingId) {
-        let loci: Loci = EmptyLoci
-        const _loci = visual.getLoci(pickingId)
-        if (!isEmptyLoci(_loci)) loci = _loci
-        return loci
+        return visual ? visual.getLoci(pickingId) : EmptyLoci
     }
 
     function mark(loci: Loci, action: MarkerAction) {
-        visual.mark(loci, action)
+        return visual ? visual.mark(loci, action) : false
     }
 
     function destroy() {
-        visual.destroy()
+        if (visual) visual.destroy()
     }
 
     return {
-        get renderObjects() { return [ visual.renderObject ] },
+        label,
+        get renderObjects() {
+            return visual && visual.renderObject ? [ visual.renderObject ] : []
+        },
         get props() { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci,
         mark,
         destroy
diff --git a/src/mol-geo/representation/structure/complex-visual.ts b/src/mol-geo/representation/structure/complex-visual.ts
index 063ecf36dc20499485644fb736b9f853f2b1b4bf..53a59c6d4ed75890f3a3463971c138f14445192b 100644
--- a/src/mol-geo/representation/structure/complex-visual.ts
+++ b/src/mol-geo/representation/structure/complex-visual.ts
@@ -15,7 +15,7 @@ import { StructureProps, DefaultStructureMeshProps, MeshUpdateState } from '.';
 import { deepEqual, ValueCell } from 'mol-util';
 import { updateMeshValues, updateRenderableState } from '../util';
 import { PickingId } from '../../util/picking';
-import { Loci, isEveryLoci } from 'mol-model/loci';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
 import { Interval } from 'mol-data/int';
 
@@ -39,62 +39,85 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe
     const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
     const updateState = MeshUpdateState.create()
 
-    let renderObject: MeshRenderObject
+    let renderObject: MeshRenderObject | undefined
     let currentProps: P
     let mesh: Mesh
     let currentStructure: Structure
     let locationIt: LocationIterator
+    let conformationHash: number
 
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) {
-            currentProps = Object.assign({}, defaultProps, props)
-            currentStructure = structure
+    async function create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) {
+        currentProps = Object.assign({}, defaultProps, props)
+        currentProps.colorTheme.structure = structure
+        currentStructure = structure
 
-            mesh = await createMesh(ctx, currentStructure, currentProps, mesh)
+        conformationHash = Structure.conformationHash(currentStructure)
+        mesh = await createMesh(ctx, currentStructure, currentProps, mesh)
 
-            locationIt = createLocationIterator(structure)
-            renderObject = createComplexMeshRenderObject(structure, mesh, locationIt, currentProps)
-        },
-        async update(ctx: RuntimeContext, props: Partial<P>) {
-            const newProps = Object.assign({}, currentProps, props)
+        locationIt = createLocationIterator(structure)
+        renderObject = await createComplexMeshRenderObject(ctx, structure, mesh, locationIt, currentProps)
+    }
 
-            if (!renderObject) return false
+    async function update(ctx: RuntimeContext, props: Partial<P>) {
+        const newProps = Object.assign({}, currentProps, props)
+        newProps.colorTheme.structure = currentStructure
 
-            locationIt.reset()
-            MeshUpdateState.reset(updateState)
-            setUpdateState(updateState, newProps, currentProps)
+        if (!renderObject) return false
 
-            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
-                updateState.createMesh = true
-            }
+        locationIt.reset()
+        MeshUpdateState.reset(updateState)
+        setUpdateState(updateState, newProps, currentProps)
 
-            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
-                updateState.updateColor = true
-            }
+        const newConformationHash = Structure.conformationHash(currentStructure)
+        if (newConformationHash !== conformationHash) {
+            conformationHash = newConformationHash
+            updateState.createMesh = true
+        }
 
-            //
+        if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true
+        if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true
+        // if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true // TODO
 
-            if (updateState.createMesh) {
-                mesh = await createMesh(ctx, currentStructure, newProps, mesh)
-                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
-                updateState.updateColor = true
-            }
+        //
 
-            if (updateState.updateColor) {
-                createColors(locationIt, newProps.colorTheme, renderObject.values)
-            }
+        if (updateState.createMesh) {
+            mesh = await createMesh(ctx, currentStructure, newProps, mesh)
+            ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+            updateState.updateColor = true
+        }
+
+        if (updateState.updateColor) {
+            await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+        }
 
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
+        updateMeshValues(renderObject.values, newProps)
+        updateRenderableState(renderObject.state, newProps)
+
+        currentProps = newProps
+        return true
+    }
 
-            currentProps = newProps
-            return true
+    return {
+        get renderObject () { return renderObject },
+        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structure?: Structure) {
+            if (!structure && !currentStructure) {
+                throw new Error('missing structure')
+            } else if (structure && (!currentStructure || !renderObject)) {
+                await create(ctx, structure, props)
+            } else if (structure && structure.hashCode !== currentStructure.hashCode) {
+                await create(ctx, structure, props)
+            } else {
+                if (structure && Structure.conformationHash(structure) !== Structure.conformationHash(currentStructure)) {
+                    currentStructure = structure
+                }
+                await update(ctx, props)
+            }
         },
         getLoci(pickingId: PickingId) {
-            return getLoci(pickingId, currentStructure, renderObject.id)
+            return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
             const { tMarker } = renderObject.values
             const { groupCount, instanceCount } = locationIt
 
@@ -106,17 +129,18 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe
 
             let changed = false
             if (isEveryLoci(loci)) {
-                apply(Interval.ofBounds(0, groupCount * instanceCount))
-                changed = true
+                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, currentStructure, apply)
             }
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/index.ts b/src/mol-geo/representation/structure/index.ts
index 06a4cba0e2a23ef2b0b742256298f705a1675a3b..7628d9e084ba81ff6ef7e9705638532e73ee2c77 100644
--- a/src/mol-geo/representation/structure/index.ts
+++ b/src/mol-geo/representation/structure/index.ts
@@ -27,17 +27,20 @@ export const DefaultStructureMeshProps = {
 export type StructureMeshProps = typeof DefaultStructureMeshProps
 
 export interface MeshUpdateState {
+    updateTransform: boolean
     updateColor: boolean
     createMesh: boolean
 }
 export namespace MeshUpdateState {
     export function create(): MeshUpdateState {
         return {
+            updateTransform: false,
             updateColor: false,
             createMesh: false
         }
     }
     export function reset(state: MeshUpdateState) {
+        state.updateTransform = false
         state.updateColor = false
         state.createMesh = false
     }
diff --git a/src/mol-geo/representation/structure/representation/backbone.ts b/src/mol-geo/representation/structure/representation/backbone.ts
index 2afb0c8cfde3121f807a4ba2e83c998f5af7bbcc..2d618326baff0370501be0557e13a74a6bce2a71 100644
--- a/src/mol-geo/representation/structure/representation/backbone.ts
+++ b/src/mol-geo/representation/structure/representation/backbone.ts
@@ -11,40 +11,39 @@ import { Task } from 'mol-task';
 import { Loci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { PolymerBackboneVisual, DefaultPolymerBackboneProps } from '../visual/polymer-backbone-cylinder';
+import { getQualityProps } from '../../util';
 
 export const DefaultBackboneProps = {
     ...DefaultPolymerBackboneProps
 }
 export type BackboneProps = typeof DefaultBackboneProps
 
-export function BackboneRepresentation(): StructureRepresentation<BackboneProps> {
-    const traceRepr = UnitsRepresentation(PolymerBackboneVisual)
+export type BackboneRepresentation = StructureRepresentation<BackboneProps>
+
+export function BackboneRepresentation(): BackboneRepresentation {
+    const traceRepr = UnitsRepresentation('Polymer backbone cylinder', PolymerBackboneVisual)
 
     let currentProps: BackboneProps
     return {
+        label: 'Backbone',
         get renderObjects() {
             return [ ...traceRepr.renderObjects ]
         },
         get props() {
             return { ...traceRepr.props }
         },
-        create: (structure: Structure, props: Partial<BackboneProps> = {}) => {
-            currentProps = Object.assign({}, DefaultBackboneProps, props)
+        createOrUpdate: (props: Partial<BackboneProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultBackboneProps, currentProps, props, qualityProps)
             return Task.create('BackboneRepresentation', async ctx => {
-                await traceRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<BackboneProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating BackboneRepresentation', async ctx => {
-                await traceRepr.update(currentProps).runInContext(ctx)
+                await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
             return traceRepr.getLoci(pickingId)
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            traceRepr.mark(loci, action)
+            return traceRepr.mark(loci, action)
         },
         destroy() {
             traceRepr.destroy()
diff --git a/src/mol-geo/representation/structure/representation/ball-and-stick.ts b/src/mol-geo/representation/structure/representation/ball-and-stick.ts
index d26ab096839de45dba420f8cd18c14da374f3c1f..c031185cf1fcd557487dd50550fd6da3757c4453 100644
--- a/src/mol-geo/representation/structure/representation/ball-and-stick.ts
+++ b/src/mol-geo/representation/structure/representation/ball-and-stick.ts
@@ -14,43 +14,40 @@ import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { InterUnitLinkVisual } from '../visual/inter-unit-link-cylinder';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
 export const DefaultBallAndStickProps = {
     ...DefaultElementSphereProps,
     ...DefaultIntraUnitLinkProps,
 
-    sizeTheme: { name: 'uniform', value: 0.25 } as SizeThemeProps,
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
     unitKinds: [ Unit.Kind.Atomic ] as Unit.Kind[]
 }
 export type BallAndStickProps = typeof DefaultBallAndStickProps
 
-export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> {
-    const elmementRepr = UnitsRepresentation(ElementSphereVisual)
-    const intraLinkRepr = UnitsRepresentation(IntraUnitLinkVisual)
-    const interLinkRepr = ComplexRepresentation(InterUnitLinkVisual)
+export type BallAndStickRepresentation = StructureRepresentation<BallAndStickProps>
+
+export function BallAndStickRepresentation(): BallAndStickRepresentation {
+    const elmementRepr = UnitsRepresentation('Element sphere mesh', ElementSphereVisual)
+    const intraLinkRepr = UnitsRepresentation('Intra-unit link cylinder', IntraUnitLinkVisual)
+    const interLinkRepr = ComplexRepresentation('Inter-unit link cylinder', InterUnitLinkVisual)
 
     let currentProps: BallAndStickProps
     return {
+        label: 'Ball & Stick',
         get renderObjects() {
             return [ ...elmementRepr.renderObjects, ...intraLinkRepr.renderObjects, ...interLinkRepr.renderObjects ]
         },
         get props() {
             return { ...elmementRepr.props, ...intraLinkRepr.props, ...interLinkRepr.props }
         },
-        create: (structure: Structure, props: Partial<BallAndStickProps> = {}) => {
-            currentProps = Object.assign({}, DefaultBallAndStickProps, props)
-            return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await elmementRepr.create(structure, currentProps).runInContext(ctx)
-                await intraLinkRepr.create(structure, currentProps).runInContext(ctx)
-                await interLinkRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<BallAndStickProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating BallAndStickRepresentation', async ctx => {
-                await elmementRepr.update(currentProps).runInContext(ctx)
-                await intraLinkRepr.update(currentProps).runInContext(ctx)
-                await interLinkRepr.update(currentProps).runInContext(ctx)
+        createOrUpdate: (props: Partial<BallAndStickProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultBallAndStickProps, currentProps, props, qualityProps)
+            return Task.create('BallAndStickRepresentation', async ctx => {
+                await elmementRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await intraLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await interLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
@@ -68,9 +65,10 @@ export function BallAndStickRepresentation(): StructureRepresentation<BallAndSti
             }
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            elmementRepr.mark(loci, action)
-            intraLinkRepr.mark(loci, action)
-            interLinkRepr.mark(loci, action)
+            const markElement = elmementRepr.mark(loci, action)
+            const markIntraLink = intraLinkRepr.mark(loci, action)
+            const markInterLink = interLinkRepr.mark(loci, action)
+            return markElement || markIntraLink || markInterLink
         },
         destroy() {
             elmementRepr.destroy()
diff --git a/src/mol-geo/representation/structure/representation/carbohydrate.ts b/src/mol-geo/representation/structure/representation/carbohydrate.ts
index bc877da0f8223269ce86771643282d18dda58a0e..46220707b4a823d84dc9c6d3777f18b8a348bb89 100644
--- a/src/mol-geo/representation/structure/representation/carbohydrate.ts
+++ b/src/mol-geo/representation/structure/representation/carbohydrate.ts
@@ -12,37 +12,38 @@ import { Loci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { CarbohydrateSymbolVisual, DefaultCarbohydrateSymbolProps } from '../visual/carbohydrate-symbol-mesh';
 import { CarbohydrateLinkVisual, DefaultCarbohydrateLinkProps } from '../visual/carbohydrate-link-cylinder';
+import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
-export const DefaultCartoonProps = {
+export const DefaultCarbohydrateProps = {
     ...DefaultCarbohydrateSymbolProps,
-    ...DefaultCarbohydrateLinkProps
+    ...DefaultCarbohydrateLinkProps,
+
+    sizeTheme: { name: 'uniform', value: 1, factor: 1 } as SizeThemeProps,
 }
-export type CarbohydrateProps = typeof DefaultCartoonProps
+export type CarbohydrateProps = typeof DefaultCarbohydrateProps
+
+export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateProps>
 
-export function CarbohydrateRepresentation(): StructureRepresentation<CarbohydrateProps> {
-    const carbohydrateSymbolRepr = ComplexRepresentation(CarbohydrateSymbolVisual)
-    const carbohydrateLinkRepr = ComplexRepresentation(CarbohydrateLinkVisual)
+export function CarbohydrateRepresentation(): CarbohydrateRepresentation {
+    const carbohydrateSymbolRepr = ComplexRepresentation('Carbohydrate symbol mesh', CarbohydrateSymbolVisual)
+    const carbohydrateLinkRepr = ComplexRepresentation('Carbohydrate link cylinder', CarbohydrateLinkVisual)
 
     let currentProps: CarbohydrateProps
     return {
+        label: 'Carbohydrate',
         get renderObjects() {
             return [ ...carbohydrateSymbolRepr.renderObjects, ...carbohydrateLinkRepr.renderObjects ]
         },
         get props() {
             return { ...carbohydrateSymbolRepr.props, ...carbohydrateLinkRepr.props }
         },
-        create: (structure: Structure, props: Partial<CarbohydrateProps> = {} as CarbohydrateProps) => {
-            currentProps = Object.assign({}, DefaultCartoonProps, props)
+        createOrUpdate: (props: Partial<CarbohydrateProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultCarbohydrateProps, currentProps, props, qualityProps)
             return Task.create('Creating CarbohydrateRepresentation', async ctx => {
-                await carbohydrateSymbolRepr.create(structure, currentProps).runInContext(ctx)
-                await carbohydrateLinkRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<CarbohydrateProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating CarbohydrateRepresentation', async ctx => {
-                await carbohydrateSymbolRepr.update(currentProps).runInContext(ctx)
-                await carbohydrateLinkRepr.update(currentProps).runInContext(ctx)
+                await carbohydrateSymbolRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await carbohydrateLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
@@ -52,8 +53,9 @@ export function CarbohydrateRepresentation(): StructureRepresentation<Carbohydra
                 : carbohydrateLinkLoci
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            carbohydrateSymbolRepr.mark(loci, action)
-            carbohydrateLinkRepr.mark(loci, action)
+            const markSymbol = carbohydrateSymbolRepr.mark(loci, action)
+            const markLink = carbohydrateLinkRepr.mark(loci, action)
+            return markSymbol || markLink
         },
         destroy() {
             carbohydrateSymbolRepr.destroy()
diff --git a/src/mol-geo/representation/structure/representation/cartoon.ts b/src/mol-geo/representation/structure/representation/cartoon.ts
index bc9b43e09a2c19f225b88a7ca122b4cbf362d807..2bafe115fc5788ef70a58f9e5b075ec515126d01 100644
--- a/src/mol-geo/representation/structure/representation/cartoon.ts
+++ b/src/mol-geo/representation/structure/representation/cartoon.ts
@@ -13,24 +13,31 @@ import { MarkerAction } from '../../../util/marker-data';
 import { PolymerTraceVisual, DefaultPolymerTraceProps } from '../visual/polymer-trace-mesh';
 import { PolymerGapVisual, DefaultPolymerGapProps } from '../visual/polymer-gap-cylinder';
 import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from '../visual/nucleotide-block-mesh';
-import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge';
+import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
+// import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge';
 
 export const DefaultCartoonProps = {
     ...DefaultPolymerTraceProps,
     ...DefaultPolymerGapProps,
     ...DefaultNucleotideBlockProps,
-    ...DefaultPolymerDirectionProps
+    // ...DefaultPolymerDirectionProps,
+
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
 }
 export type CartoonProps = typeof DefaultCartoonProps
 
-export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
-    const traceRepr = UnitsRepresentation(PolymerTraceVisual)
-    const gapRepr = UnitsRepresentation(PolymerGapVisual)
-    const blockRepr = UnitsRepresentation(NucleotideBlockVisual)
-    const directionRepr = UnitsRepresentation(PolymerDirectionVisual)
+export type CartoonRepresentation = StructureRepresentation<CartoonProps>
+
+export function CartoonRepresentation(): CartoonRepresentation {
+    const traceRepr = UnitsRepresentation('Polymer trace mesh', PolymerTraceVisual)
+    const gapRepr = UnitsRepresentation('Polymer gap cylinder', PolymerGapVisual)
+    const blockRepr = UnitsRepresentation('Nucleotide block mesh', NucleotideBlockVisual)
+    // const directionRepr = UnitsRepresentation('Polymer direction wedge', PolymerDirectionVisual)
 
     let currentProps: CartoonProps
     return {
+        label: 'Cartoon',
         get renderObjects() {
             return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects,
                 ...blockRepr.renderObjects // , ...directionRepr.renderObjects
@@ -39,45 +46,39 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
         get props() {
             return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props }
         },
-        create: (structure: Structure, props: Partial<CartoonProps> = {}) => {
-            currentProps = Object.assign({}, DefaultCartoonProps, props)
+        createOrUpdate: (props: Partial<CartoonProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultCartoonProps, currentProps, props, qualityProps)
             return Task.create('Creating CartoonRepresentation', async ctx => {
-                await traceRepr.create(structure, currentProps).runInContext(ctx)
-                await gapRepr.create(structure, currentProps).runInContext(ctx)
-                await blockRepr.create(structure, currentProps).runInContext(ctx)
-                await directionRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<CartoonProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating CartoonRepresentation', async ctx => {
-                await traceRepr.update(currentProps).runInContext(ctx)
-                await gapRepr.update(currentProps).runInContext(ctx)
-                await blockRepr.update(currentProps).runInContext(ctx)
-                await directionRepr.update(currentProps).runInContext(ctx)
+                await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await gapRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                await blockRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
+                // await directionRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
             const traceLoci = traceRepr.getLoci(pickingId)
             const gapLoci = gapRepr.getLoci(pickingId)
             const blockLoci = blockRepr.getLoci(pickingId)
-            const directionLoci = directionRepr.getLoci(pickingId)
+            // const directionLoci = directionRepr.getLoci(pickingId)
             return !isEmptyLoci(traceLoci) ? traceLoci
                 : !isEmptyLoci(gapLoci) ? gapLoci
-                : !isEmptyLoci(blockLoci) ? blockLoci
-                : directionLoci
+                : blockLoci
+                // : !isEmptyLoci(blockLoci) ? blockLoci
+                // : directionLoci
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            traceRepr.mark(loci, action)
-            gapRepr.mark(loci, action)
-            blockRepr.mark(loci, action)
-            directionRepr.mark(loci, action)
+            const markTrace = traceRepr.mark(loci, action)
+            const markGap = gapRepr.mark(loci, action)
+            const markBlock = blockRepr.mark(loci, action)
+            // const markDirection = directionRepr.mark(loci, action)
+            return markTrace || markGap || markBlock // \\ markDirection
         },
         destroy() {
             traceRepr.destroy()
             gapRepr.destroy()
             blockRepr.destroy()
-            directionRepr.destroy()
+            // directionRepr.destroy()
         }
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/representation/distance-restraint.ts b/src/mol-geo/representation/structure/representation/distance-restraint.ts
index bf16518eb73e61c16ad4a8b1395a02eca2c4ac38..d1ed2524ee9dcd68226d126442ca945d9f374fa7 100644
--- a/src/mol-geo/representation/structure/representation/distance-restraint.ts
+++ b/src/mol-geo/representation/structure/representation/distance-restraint.ts
@@ -12,6 +12,7 @@ import { Loci } from 'mol-model/loci';
 import { MarkerAction } from '../../../util/marker-data';
 import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from '../visual/cross-link-restraint-cylinder';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { getQualityProps } from '../../util';
 
 export const DefaultDistanceRestraintProps = {
     ...DefaultCrossLinkRestraintProps,
@@ -19,34 +20,32 @@ export const DefaultDistanceRestraintProps = {
 }
 export type DistanceRestraintProps = typeof DefaultDistanceRestraintProps
 
-export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> {
-    const crossLinkRepr = ComplexRepresentation(CrossLinkRestraintVisual)
+export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintProps>
+
+export function DistanceRestraintRepresentation(): DistanceRestraintRepresentation {
+    const crossLinkRepr = ComplexRepresentation('Cross-link restraint', CrossLinkRestraintVisual)
 
     let currentProps: DistanceRestraintProps
     return {
+        label: 'Distance restraint',
         get renderObjects() {
             return [ ...crossLinkRepr.renderObjects ]
         },
         get props() {
             return { ...crossLinkRepr.props }
         },
-        create: (structure: Structure, props: Partial<DistanceRestraintProps> = {}) => {
-            currentProps = Object.assign({}, DefaultDistanceRestraintProps, props)
+        createOrUpdate: (props: Partial<DistanceRestraintProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultDistanceRestraintProps, currentProps, props, qualityProps)
             return Task.create('DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.create(structure, currentProps).runInContext(ctx)
-            })
-        },
-        update: (props: Partial<DistanceRestraintProps>) => {
-            currentProps = Object.assign(currentProps, props)
-            return Task.create('Updating DistanceRestraintRepresentation', async ctx => {
-                await crossLinkRepr.update(currentProps).runInContext(ctx)
+                await crossLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx)
             })
         },
         getLoci: (pickingId: PickingId) => {
             return crossLinkRepr.getLoci(pickingId)
         },
         mark: (loci: Loci, action: MarkerAction) => {
-            crossLinkRepr.mark(loci, action)
+            return crossLinkRepr.mark(loci, action)
         },
         destroy() {
             crossLinkRepr.destroy()
diff --git a/src/mol-geo/representation/structure/representation/point.ts b/src/mol-geo/representation/structure/representation/point.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9c388317cd9badd0c37899a7833201cb20831d9b
--- /dev/null
+++ b/src/mol-geo/representation/structure/representation/point.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { UnitsRepresentation } from '..';
+import { ElementPointVisual, DefaultElementPointProps } from '../visual/element-point';
+import { StructureRepresentation } from '../units-representation';
+import { Structure } from 'mol-model/structure';
+import { MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { PickingId } from '../../../util/picking';
+
+export const DefaultPointProps = {
+    ...DefaultElementPointProps,
+}
+export type PointProps = typeof DefaultPointProps
+
+export type PointRepresentation = StructureRepresentation<PointProps>
+
+export function PointRepresentation(): PointRepresentation {
+    let currentProps: PointProps
+    const pointRepr = UnitsRepresentation('Point', ElementPointVisual)
+    return {
+        label: 'Point',
+        get renderObjects() {
+            return [ ...pointRepr.renderObjects ]
+        },
+        get props() {
+            return { ...pointRepr.props }
+        },
+        createOrUpdate: (props: Partial<PointProps> = {}, structure?: Structure) => {
+            currentProps = Object.assign({}, DefaultPointProps, currentProps, props)
+            return pointRepr.createOrUpdate(currentProps, structure)
+        },
+        getLoci: (pickingId: PickingId) => {
+            return pointRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            return pointRepr.mark(loci, action)
+        },
+        destroy() {
+            pointRepr.destroy()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/representation/spacefill.ts b/src/mol-geo/representation/structure/representation/spacefill.ts
index d2062b2975e6e3efe568520b26c439447dec7870..5de4260277c617a03b0012cab38c7de44aeb5686 100644
--- a/src/mol-geo/representation/structure/representation/spacefill.ts
+++ b/src/mol-geo/representation/structure/representation/spacefill.ts
@@ -6,12 +6,44 @@
 
 import { UnitsRepresentation } from '..';
 import { ElementSphereVisual, DefaultElementSphereProps } from '../visual/element-sphere';
+import { StructureRepresentation } from '../units-representation';
+import { Structure } from 'mol-model/structure';
+import { PickingId } from '../../../util/picking';
+import { MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { getQualityProps } from '../../util';
 
 export const DefaultSpacefillProps = {
     ...DefaultElementSphereProps
 }
 export type SpacefillProps = typeof DefaultSpacefillProps
 
-export function SpacefillRepresentation() {
-    return UnitsRepresentation(ElementSphereVisual)
+export type SpacefillRepresentation = StructureRepresentation<SpacefillProps>
+
+export function SpacefillRepresentation(): SpacefillRepresentation {
+    let currentProps: SpacefillProps
+    const sphereRepr = UnitsRepresentation('Sphere mesh', ElementSphereVisual)
+    return {
+        label: 'Spacefill',
+        get renderObjects() {
+            return [ ...sphereRepr.renderObjects ]
+        },
+        get props() {
+            return { ...sphereRepr.props }
+        },
+        createOrUpdate: (props: Partial<SpacefillProps> = {}, structure?: Structure) => {
+            const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure)
+            currentProps = Object.assign({}, DefaultSpacefillProps, currentProps, props, qualityProps)
+            return sphereRepr.createOrUpdate(currentProps, structure)
+        },
+        getLoci: (pickingId: PickingId) => {
+            return sphereRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            return sphereRepr.mark(loci, action)
+        },
+        destroy() {
+            sphereRepr.destroy()
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/units-representation.ts b/src/mol-geo/representation/structure/units-representation.ts
index f2ed113e7b621b7db6d0ef4827f9f4c9d019a9be..8b7e9ec5e2c22db5460a928eec7ecf2d254d35ce 100644
--- a/src/mol-geo/representation/structure/units-representation.ts
+++ b/src/mol-geo/representation/structure/units-representation.ts
@@ -12,87 +12,99 @@ import { Representation, RepresentationProps, Visual } from '..';
 import { PickingId } from '../../util/picking';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
 import { MarkerAction } from '../../util/marker-data';
-import { getQualityProps } from '../util';
-import { DefaultStructureProps, StructureProps } from '.';
+import { StructureProps } from '.';
+import { StructureGroup } from './units-visual';
 
-export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 export interface  StructureVisual<P extends RepresentationProps = {}> extends Visual<Structure, P> { }
 
 export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { }
 
-export function UnitsRepresentation<P extends StructureProps>(visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
+export function UnitsRepresentation<P extends StructureProps>(label: string, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
     let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
 
     let _props: P
     let _structure: Structure
     let _groups: ReadonlyArray<Unit.SymmetryGroup>
 
-    function create(structure: Structure, props: Partial<P> = {}) {
-        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
-        _props.colorTheme!.structure = structure
+    function createOrUpdate(props: Partial<P> = {}, structure?: Structure) {
+        _props = Object.assign({}, _props, props)
 
-        return Task.create('Creating StructureRepresentation', async ctx => {
-            if (!_structure) {
+        return Task.create('Creating or updating StructureRepresentation', async ctx => {
+            if (!_structure && !structure) {
+                throw new Error('missing structure')
+            } else if (structure && !_structure) {
+                // console.log('initial structure')
+                // First call with a structure, create visuals for each group.
                 _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
                     const visual = visualCtor()
-                    await visual.create(ctx, group, _props)
+                    await visual.createOrUpdate(ctx, _props, { group, structure })
                     visuals.set(group.hashCode, { visual, group })
                 }
-            } else {
-                if (_structure.hashCode === structure.hashCode) {
-                    await update(_props)
-                } else {
-                    _groups = structure.unitSymmetryGroups;
-                    const newGroups: Unit.SymmetryGroup[] = []
-                    const oldUnitsVisuals = visuals
-                    visuals = new Map()
-                    for (let i = 0; i < _groups.length; i++) {
-                        const group = _groups[i];
-                        const visualGroup = oldUnitsVisuals.get(group.hashCode)
-                        if (visualGroup) {
-                            const { visual, group } = visualGroup
-                            if (!await visual.update(ctx, _props)) {
-                                await visual.create(ctx, group, _props)
-                            }
-                            oldUnitsVisuals.delete(group.hashCode)
-                        } else {
-                            newGroups.push(group)
-                            const visual = visualCtor()
-                            await visual.create(ctx, group, _props)
-                            visuals.set(group.hashCode, { visual, group })
-                        }
+            } else if (structure && _structure.hashCode !== structure.hashCode) {
+                // console.log('_structure.hashCode !== structure.hashCode')
+                // Tries to re-use existing visuals for the groups of the new structure.
+                // Creates additional visuals if needed, destroys left-over visuals.
+                _groups = structure.unitSymmetryGroups;
+                // const newGroups: Unit.SymmetryGroup[] = []
+                const oldVisuals = visuals
+                visuals = new Map()
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visualGroup = oldVisuals.get(group.hashCode)
+                    if (visualGroup) {
+                        const { visual } = visualGroup
+                        await visual.createOrUpdate(ctx, _props, { group, structure })
+                        visuals.set(group.hashCode, { visual, group })
+                        oldVisuals.delete(group.hashCode)
+                    } else {
+                        // newGroups.push(group)
+                        const visual = visualCtor()
+                        await visual.createOrUpdate(ctx, _props, { group, structure })
+                        visuals.set(group.hashCode, { visual, group })
                     }
+                }
+                oldVisuals.forEach(({ visual }) => visual.destroy())
 
-                    // for new groups, re-use leftover visuals
-                    const unusedVisuals: UnitsVisual<P>[] = []
-                    oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
-                    newGroups.forEach(async group => {
-                        const visual = unusedVisuals.pop() || visualCtor()
-                        await visual.create(ctx, group, _props)
-                        visuals.set(group.hashCode, { visual, group })
-                    })
-                    unusedVisuals.forEach(visual => visual.destroy())
+                // TODO review logic
+                // For new groups, re-use left-over visuals
+                // const unusedVisuals: UnitsVisual<P>[] = []
+                // oldVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
+                // newGroups.forEach(async group => {
+                //     const visual = unusedVisuals.pop() || visualCtor()
+                //     await visual.createOrUpdate(ctx, _props, group)
+                //     visuals.set(group.hashCode, { visual, group })
+                // })
+                // unusedVisuals.forEach(visual => visual.destroy())
+            } else if (structure && _structure.hashCode === structure.hashCode) {
+                // console.log('_structure.hashCode === structure.hashCode')
+                // Expects that for structures with the same hashCode,
+                // the unitSymmetryGroups are the same as well.
+                // Re-uses existing visuals for the groups of the new structure.
+                _groups = structure.unitSymmetryGroups;
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visualGroup = visuals.get(group.hashCode)
+                    if (visualGroup) {
+                        await visualGroup.visual.createOrUpdate(ctx, _props, { group, structure })
+                        visualGroup.group = group
+                    } else {
+                        throw new Error(`expected to find visual for hashCode ${group.hashCode}`)
+                    }
                 }
+            } else {
+                // console.log('no new structure')
+                // No new structure given, just update all visuals with new props.
+                visuals.forEach(async ({ visual, group }) => {
+                    await visual.createOrUpdate(ctx, _props, { group, structure: _structure })
+                })
             }
-            _structure = structure
+            if (structure) _structure = structure
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('Updating StructureRepresentation', async ctx => {
-            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
-            _props.colorTheme!.structure = _structure
-
-            visuals.forEach(async ({ visual, group }) => {
-                if (!await visual.update(ctx, _props)) {
-                    await visual.create(ctx, group, _props)
-                }
-            })
-        })
-    }
-
     function getLoci(pickingId: PickingId) {
         let loci: Loci = EmptyLoci
         visuals.forEach(({ visual }) => {
@@ -103,7 +115,11 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () =>
     }
 
     function mark(loci: Loci, action: MarkerAction) {
-        visuals.forEach(({ visual }) => visual.mark(loci, action))
+        let changed = false
+        visuals.forEach(({ visual }) => {
+            changed = visual.mark(loci, action) || changed
+        })
+        return changed
     }
 
     function destroy() {
@@ -112,16 +128,18 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () =>
     }
 
     return {
+        label,
         get renderObjects() {
             const renderObjects: RenderObject[] = []
-            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
+            visuals.forEach(({ visual }) => {
+                if (visual.renderObject) renderObjects.push(visual.renderObject)
+            })
             return renderObjects
         },
         get props() {
             return _props
         },
-        create,
-        update,
+        createOrUpdate,
         getLoci,
         mark,
         destroy
diff --git a/src/mol-geo/representation/structure/units-visual.ts b/src/mol-geo/representation/structure/units-visual.ts
index d7ebb4b9bc5ccc140c3091671ba836e8fbc87f6a..534764f3d57028e1ad33103d4e90125d3c69d4fe 100644
--- a/src/mol-geo/representation/structure/units-visual.ts
+++ b/src/mol-geo/representation/structure/units-visual.ts
@@ -4,22 +4,25 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit } from 'mol-model/structure';
+import { Unit, Structure } from 'mol-model/structure';
 import { RepresentationProps, Visual } from '..';
 import { DefaultStructureMeshProps, MeshUpdateState } from '.';
 import { RuntimeContext } from 'mol-task';
 import { PickingId } from '../../util/picking';
 import { LocationIterator } from '../../util/location-iterator';
 import { Mesh } from '../../mesh/mesh';
-import { MarkerAction, applyMarkerAction } from '../../util/marker-data';
-import { Loci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
 import { MeshRenderObject } from 'mol-gl/render-object';
 import { createUnitsMeshRenderObject, createColors } from './visual/util/common';
-import { deepEqual, ValueCell } from 'mol-util';
+import { deepEqual, ValueCell, UUID } from 'mol-util';
 import { updateMeshValues, updateRenderableState } from '../util';
 import { Interval } from 'mol-data/int';
+import { createTransforms } from '../../util/transform-data';
 
-export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
+
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
 export const DefaultUnitsMeshProps = {
     ...DefaultStructureMeshProps,
@@ -40,66 +43,104 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
     const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder
     const updateState = MeshUpdateState.create()
 
-    let renderObject: MeshRenderObject
+    let renderObject: MeshRenderObject | undefined
     let currentProps: P
     let mesh: Mesh
     let currentGroup: Unit.SymmetryGroup
+    let currentStructure: Structure
     let locationIt: LocationIterator
+    let currentConformationId: UUID
+
+    async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
+        currentProps = Object.assign({}, defaultProps, props)
+        currentProps.colorTheme.structure = currentStructure
+        currentGroup = group
+
+        const unit = group.units[0]
+        currentConformationId = Unit.conformationId(unit)
+        mesh = currentProps.unitKinds.includes(unit.kind)
+            ? await createMesh(ctx, unit, currentProps, mesh)
+            : Mesh.createEmpty(mesh)
+
+        // TODO create empty location iterator when not in unitKinds
+        locationIt = createLocationIterator(group)
+        renderObject = await createUnitsMeshRenderObject(ctx, group, mesh, locationIt, currentProps)
+    }
 
-    return {
-        get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) {
-            currentProps = Object.assign({}, defaultProps, props)
-            currentGroup = group
+    async function update(ctx: RuntimeContext, props: Partial<P> = {}) {
+        if (!renderObject) return
 
-            const unit = group.units[0]
-            mesh = currentProps.unitKinds.includes(unit.kind)
-                ? await createMesh(ctx, unit, currentProps, mesh)
-                : Mesh.createEmpty(mesh)
+        const newProps = Object.assign({}, currentProps, props)
+        newProps.colorTheme.structure = currentStructure
+        const unit = currentGroup.units[0]
 
-            locationIt = createLocationIterator(group)
-            renderObject = createUnitsMeshRenderObject(group, mesh, locationIt, currentProps)
-        },
-        async update(ctx: RuntimeContext, props: Partial<P>) {
-            const newProps = Object.assign({}, currentProps, props)
-            const unit = currentGroup.units[0]
+        locationIt.reset()
+        MeshUpdateState.reset(updateState)
+        setUpdateState(updateState, newProps, currentProps)
 
-            if (!renderObject) return false
+        const newConformationId = Unit.conformationId(unit)
+        if (newConformationId !== currentConformationId) {
+            currentConformationId = newConformationId
+            updateState.createMesh = true
+        }
 
-            locationIt.reset()
-            MeshUpdateState.reset(updateState)
-            setUpdateState(updateState, newProps, currentProps)
+        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
 
-            if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) {
-                updateState.createMesh = true
-            }
+        if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true
+        if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true
+        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true
 
-            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
-                updateState.updateColor = true
-            }
+        //
 
-            //
+        if (updateState.updateTransform) {
+            locationIt = createLocationIterator(currentGroup)
+            const { instanceCount, groupCount } = locationIt
+            createTransforms(currentGroup, renderObject.values)
+            createMarkers(instanceCount * groupCount, renderObject.values)
+            updateState.updateColor = true
+        }
 
-            if (updateState.createMesh) {
-                mesh = await createMesh(ctx, unit, newProps, mesh)
-                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
-                updateState.updateColor = true
-            }
+        if (updateState.createMesh) {
+            mesh = newProps.unitKinds.includes(unit.kind)
+                ? await createMesh(ctx, unit, newProps, mesh)
+                : Mesh.createEmpty(mesh)
+            ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+            updateState.updateColor = true
+        }
 
-            if (updateState.updateColor) {
-                createColors(locationIt, newProps.colorTheme, renderObject.values)
-            }
+        if (updateState.updateColor) {
+            await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+        }
 
-            updateMeshValues(renderObject.values, newProps)
-            updateRenderableState(renderObject.state, newProps)
+        updateMeshValues(renderObject.values, newProps)
+        updateRenderableState(renderObject.state, newProps)
 
-            currentProps = newProps
-            return true
+        currentProps = newProps
+    }
+
+    return {
+        get renderObject () { return renderObject },
+        async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) {
+            if (structureGroup) currentStructure = structureGroup.structure
+            const group = structureGroup ? structureGroup.group : undefined
+            if (!group && !currentGroup) {
+                throw new Error('missing group')
+            } else if (group && (!currentGroup || !renderObject)) {
+                await create(ctx, group, props)
+            } else if (group && group.hashCode !== currentGroup.hashCode) {
+                await create(ctx, group, props)
+            } else {
+                if (group && !areGroupsIdentical(group, currentGroup)) {
+                    currentGroup = group
+                }
+                await update(ctx, props)
+            }
         },
         getLoci(pickingId: PickingId) {
-            return getLoci(pickingId, currentGroup, renderObject.id)
+            return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
             const { tMarker } = renderObject.values
             const { groupCount, instanceCount } = locationIt
 
@@ -111,17 +152,25 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu
 
             let changed = false
             if (isEveryLoci(loci)) {
-                apply(Interval.ofBounds(0, groupCount * instanceCount))
-                changed = true
+                changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, currentGroup, apply)
             }
             if (changed) {
                 ValueCell.update(tMarker, tMarker.ref.value)
             }
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
+}
+
+function areGroupsIdentical(groupA: Unit.SymmetryGroup, groupB: Unit.SymmetryGroup) {
+    return (
+        groupA.units.length === groupB.units.length &&
+        Unit.conformationId(groupA.units[0]) === Unit.conformationId(groupB.units[0])
+    )
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts
index 6526391f04326d93df99dd68f5c35c69de5d16c0..185835c629e4bcca92a85f02008992f9466199df 100644
--- a/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts
@@ -92,15 +92,15 @@ function CarbohydrateLinkIterator(structure: Structure): LocationIterator {
         const link = links[groupIndex]
         const carbA = elements[link.carbohydrateIndexA]
         const carbB = elements[link.carbohydrateIndexB]
-        const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon)
-        const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon)
+        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon)
         location.aUnit = carbA.unit
         location.aIndex = indexA as StructureElement.UnitIndex
         location.bUnit = carbB.unit
         location.bIndex = indexB as StructureElement.UnitIndex
         return location
     }
-    return LocationIterator(groupCount, instanceCount, getLocation)
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
@@ -110,14 +110,16 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
         const l = links[groupId]
         const carbA = elements[l.carbohydrateIndexA]
         const carbB = elements[l.carbohydrateIndexB]
-        const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon)
-        const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon)
-        return Link.Loci([
-            Link.Location(
-                carbA.unit, indexA as StructureElement.UnitIndex,
-                carbB.unit, indexB as StructureElement.UnitIndex
-            )
-        ])
+        const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon)
+        const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon)
+        if (indexA !== -1 && indexB !== -1) {
+            return Link.Loci([
+                Link.Location(
+                    carbA.unit, indexA as StructureElement.UnitIndex,
+                    carbB.unit, indexB as StructureElement.UnitIndex
+                )
+            ])
+        }
     }
     return EmptyLoci
 }
diff --git a/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
index 6546a2b2ac14106953307cabfb2655f3db9b7311..13d568ccb73cd097c659a36a8281c9e600b4aa85 100644
--- a/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
+++ b/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
@@ -5,7 +5,7 @@
  */
 
 import { Unit, Structure, StructureElement } from 'mol-model/structure';
-import { ComplexVisual } from '..';
+import { ComplexVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { PickingId } from '../../../util/picking';
@@ -154,7 +154,9 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolProp
         createLocationIterator: CarbohydrateElementIterator,
         getLoci: getCarbohydrateLoci,
         mark: markCarbohydrate,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: CarbohydrateSymbolProps, currentProps: CarbohydrateSymbolProps) => {
+            state.createMesh = newProps.detail !== currentProps.detail
+        }
     })
 }
 
@@ -172,7 +174,7 @@ function CarbohydrateElementIterator(structure: Structure): LocationIterator {
     function isSecondary (elementIndex: number, instanceIndex: number) {
         return (elementIndex % 2) === 1
     }
-    return LocationIterator(groupCount, instanceCount, getLocation, isSecondary)
+    return LocationIterator(groupCount, instanceCount, getLocation, true, isSecondary)
 }
 
 function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: number) {
@@ -180,9 +182,11 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num
     if (id === objectId) {
         const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)]
         const { unit } = carb
-        const index = OrderedSet.findPredecessorIndex(unit.elements, carb.anomericCarbon)
-        const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex)
-        return StructureElement.Loci([{ unit, indices }])
+        const index = OrderedSet.indexOf(unit.elements, carb.anomericCarbon)
+        if (index !== -1) {
+            const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex)
+            return StructureElement.Loci([{ unit, indices }])
+        }
     }
     return EmptyLoci
 }
diff --git a/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
index 6bd885217c6132fbbdce6ed6ed481aeb68afda33..735499fd290cee6d58f5213079613c20aed9d75b 100644
--- a/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
@@ -84,7 +84,7 @@ function CrossLinkRestraintIterator(structure: Structure): LocationIterator {
         location.bIndex = pair.indexB
         return location
     }
-    return LocationIterator(groupCount, instanceCount, getLocation)
+    return LocationIterator(groupCount, instanceCount, getLocation, true)
 }
 
 function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
diff --git a/src/mol-geo/representation/structure/visual/element-point.ts b/src/mol-geo/representation/structure/visual/element-point.ts
index e58f1bee17d71ac6a7605317fb6000a6ce448ca4..d0b74c089e9f37c81a817f653f54702e4574fabf 100644
--- a/src/mol-geo/representation/structure/visual/element-point.ts
+++ b/src/mol-geo/representation/structure/visual/element-point.ts
@@ -6,131 +6,164 @@
  */
 
 import { ValueCell } from 'mol-util/value-cell'
-import { createPointRenderObject, PointRenderObject } from 'mol-gl/render-object'
-import { Unit } from 'mol-model/structure';
+import { PointRenderObject } from 'mol-gl/render-object'
+import { Unit, Structure } from 'mol-model/structure';
 import { RuntimeContext } from 'mol-task'
-
 import { UnitsVisual, DefaultStructureProps } from '..';
-import { getElementLoci, StructureElementIterator } from './util/element';
-import { createTransforms, createColors, createSizes } from './util/common';
-import { deepEqual, defaults } from 'mol-util';
-import { SortedArray } from 'mol-data/int';
-import { RenderableState, PointValues } from 'mol-gl/renderable';
+import { getElementLoci, StructureElementIterator, markElement } from './util/element';
+import { createColors, createSizes, createUnitsPointRenderObject } from './util/common';
+import { deepEqual, UUID } from 'mol-util';
+import { Interval } from 'mol-data/int';
 import { PickingId } from '../../../util/picking';
-import { Loci } from 'mol-model/loci';
-import { MarkerAction, createMarkers } from '../../../util/marker-data';
+import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci';
+import { MarkerAction, createMarkers, applyMarkerAction } from '../../../util/marker-data';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { fillSerial } from 'mol-util/array';
 import { SizeThemeProps } from 'mol-view/theme/size';
+import { LocationIterator } from '../../../util/location-iterator';
+import { createTransforms } from '../../../util/transform-data';
+import { StructureGroup } from '../units-visual';
+import { updateRenderableState } from '../../util';
 
-export const DefaultPointProps = {
+export const DefaultElementPointProps = {
     ...DefaultStructureProps,
-    sizeTheme: { name: 'physical' } as SizeThemeProps
+
+    sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps,
+    pointSizeAttenuation: true,
 }
-export type PointProps = Partial<typeof DefaultPointProps>
+export type ElementPointProps = Partial<typeof DefaultElementPointProps>
 
-export function createPointVertices(unit: Unit) {
+export async function createElementPointVertices(ctx: RuntimeContext, unit: Unit, vertices?: ValueCell<Float32Array>) {
     const elements = unit.elements
-    const elementCount = elements.length
-    const vertices = new Float32Array(elementCount * 3)
+    const n = elements.length * 3
+    const array = vertices && vertices.ref.value.length >= n ? vertices.ref.value : new Float32Array(n)
 
     const pos = unit.conformation.invariantPosition
 
     const p = Vec3.zero()
-    for (let i = 0; i < elementCount; i++) {
-        const i3 = i * 3
-        pos(elements[i], p)
-        vertices[i3] = p[0]
-        vertices[i3 + 1] = p[1]
-        vertices[i3 + 2] = p[2]
+    for (let i = 0; i < n; i += 3) {
+        pos(elements[i / 3], p)
+        array[i] = p[0]
+        array[i + 1] = p[1]
+        array[i + 2] = p[2]
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating points', current: i / 3, max: elements.length });
+        }
+        ++i
     }
-    return vertices
+    return vertices ? ValueCell.update(vertices, array) : ValueCell.create(array)
 }
 
-export default function PointVisual(): UnitsVisual<PointProps> {
-    let renderObject: PointRenderObject
-    let currentProps = DefaultPointProps
+export function ElementPointVisual(): UnitsVisual<ElementPointProps> {
+    let renderObject: PointRenderObject | undefined
+    let currentProps = DefaultElementPointProps
     let currentGroup: Unit.SymmetryGroup
-
-    let _units: ReadonlyArray<Unit>
-    let _elements: SortedArray
+    let currentStructure: Structure
+    let locationIt: LocationIterator
+    let vertices: ValueCell<Float32Array>
+    let currentConformationId: UUID
 
     return {
         get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PointProps = {}) {
-            currentProps = Object.assign({}, DefaultPointProps, props)
-            currentGroup = group
-
-            _units = group.units
-            _elements = group.elements;
-
-            const { colorTheme, sizeTheme } = currentProps
-            const elementCount = _elements.length
-            const instanceCount = group.units.length
-
-            const locationIt = StructureElementIterator.fromGroup(group)
-
-            const vertices = createPointVertices(_units[0])
-            const transforms = createTransforms(group)
-            const color = createColors(locationIt, colorTheme)
-            const size = createSizes(locationIt, sizeTheme)
-            const marker = createMarkers(instanceCount * elementCount)
-
-            const values: PointValues = {
-                aPosition: ValueCell.create(vertices),
-                aGroup: ValueCell.create(fillSerial(new Float32Array(elementCount))),
-                aTransform: transforms,
-                aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
-                ...color,
-                ...marker,
-                ...size,
-
-                uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
-                uInstanceCount: ValueCell.create(instanceCount),
-                uGroupCount: ValueCell.create(group.elements.length),
-
-                drawCount: ValueCell.create(vertices.length / 3),
-                instanceCount: ValueCell.create(instanceCount),
-
-                dPointSizeAttenuation: ValueCell.create(true),
-                dUseFog: ValueCell.create(defaults(props.useFog, true)),
-            }
-            const state: RenderableState = {
-                depthMask: defaults(props.depthMask, true),
-                visible: defaults(props.visible, true)
+        async createOrUpdate(ctx: RuntimeContext, props: ElementPointProps = {}, structureGroup?: StructureGroup) {
+            if (structureGroup) currentStructure = structureGroup.structure
+            const group = structureGroup ? structureGroup.group : undefined
+            if (!group && !currentGroup) {
+                throw new Error('missing group')
+            } else if (group && !currentGroup) {
+                currentProps = Object.assign({}, DefaultElementPointProps, props)
+                currentProps.colorTheme.structure = currentStructure
+                currentGroup = group
+                locationIt = StructureElementIterator.fromGroup(group)
+
+                const unit = group.units[0]
+                currentConformationId = Unit.conformationId(unit)
+                vertices = await createElementPointVertices(ctx, unit, vertices)
+
+                renderObject = await createUnitsPointRenderObject(ctx, group, vertices, locationIt, currentProps)
+            } else if (renderObject) {
+                if (group) currentGroup = group
+
+                const newProps = { ...currentProps, ...props }
+                const unit = currentGroup.units[0]
+
+                let updateTransform = false
+                let createVertices = false
+                let updateColor = false
+                let updateSize = false
+
+                const newConformationId = Unit.conformationId(unit)
+                if (newConformationId !== currentConformationId) {
+                    currentConformationId = newConformationId
+                    createVertices = true
+                }
+
+                if (currentGroup.units.length !== locationIt.instanceCount) updateTransform = true
+
+                if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) createVertices = true
+                if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateColor = true
+                if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateSize = true
+
+                if (updateTransform) {
+                    locationIt = StructureElementIterator.fromGroup(currentGroup)
+                    const { instanceCount, groupCount } = locationIt
+                    createTransforms(currentGroup, renderObject.values)
+                    createMarkers(instanceCount * groupCount, renderObject.values)
+                    updateColor = true
+                    updateSize = true
+                }
+
+                if (createVertices) {
+                    await createElementPointVertices(ctx, unit, vertices)
+                    ValueCell.update(renderObject.values.aGroup, fillSerial(new Float32Array(locationIt.groupCount))) // TODO reuse array
+                    ValueCell.update(renderObject.values.drawCount, locationIt.groupCount)
+                    updateColor = true
+                    updateSize = true
+                }
+
+                if (updateColor) {
+                    await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values)
+                }
+
+                if (updateSize) {
+                    await createSizes(ctx, locationIt, newProps.sizeTheme, renderObject.values)
+                }
+
+                updateRenderableState(renderObject.state, newProps)
+
+                currentProps = newProps
             }
-
-            renderObject = createPointRenderObject(values, state)
         },
-        async update(ctx: RuntimeContext, props: PointProps) {
-            if (!renderObject || !_units || !_elements) return false
-
-            const newProps = { ...currentProps, ...props }
-            if (deepEqual(currentProps, newProps)) {
-                console.log('props identical, nothing to change')
-                return true
+        getLoci(pickingId: PickingId) {
+            return renderObject ? getElementLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            if (!renderObject) return false
+            const { tMarker } = renderObject.values
+            const { groupCount, instanceCount } = locationIt
+
+            function apply(interval: Interval) {
+                const start = Interval.start(interval)
+                const end = Interval.end(interval)
+                return applyMarkerAction(tMarker.ref.value.array, start, end, action)
             }
 
-            if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) {
-                console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme)
+            let changed = false
+            if (isEveryLoci(loci)) {
+                apply(Interval.ofBounds(0, groupCount * instanceCount))
+                changed = true
+            } else {
+                changed = markElement(loci, currentGroup, apply)
             }
-
-            if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) {
-                console.log('sizeTheme changed', currentProps.sizeTheme, newProps.sizeTheme)
+            if (changed) {
+                ValueCell.update(tMarker, tMarker.ref.value)
             }
-
-            currentProps = newProps
-            return false
-        },
-        getLoci(pickingId: PickingId) {
-            return getElementLoci(pickingId, currentGroup, renderObject.id)
-        },
-        mark(loci: Loci, action: MarkerAction) {
-            // TODO
-            // markElement(loci, action, currentGroup, renderObject.values)
+            return changed
         },
         destroy() {
             // TODO
+            renderObject = undefined
         }
     }
 }
diff --git a/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
index 813da2a3b9ce6864298fe0e8424972e05ab97b7a..44469dacff2b8bc4587e8cdb89ce9da3a15b4bff 100644
--- a/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
@@ -51,6 +51,7 @@ async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: S
 export const DefaultInterUnitLinkProps = {
     ...DefaultComplexMeshProps,
     ...DefaultLinkCylinderProps,
+
     sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
 export type InterUnitLinkProps = typeof DefaultInterUnitLinkProps
diff --git a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
index bf3e964bc6788f9b2f36e47324de31e147e6eb40..9b9452a4191d579a07837137b34ccb22082261ae 100644
--- a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
@@ -6,7 +6,7 @@
  */
 
 import { Unit, Link, StructureElement } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh, LinkIterator } from './util/link';
 import { Mesh } from '../../../mesh/mesh';
@@ -65,6 +65,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit,
 export const DefaultIntraUnitLinkProps = {
     ...DefaultUnitsMeshProps,
     ...DefaultLinkCylinderProps,
+
     sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps,
 }
 export type IntraUnitLinkProps = typeof DefaultIntraUnitLinkProps
@@ -76,7 +77,9 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
         createLocationIterator: LinkIterator.fromGroup,
         getLoci: getLinkLoci,
         mark: markLink,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: LinkCylinderProps, currentProps: LinkCylinderProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }
 
@@ -99,12 +102,13 @@ function markLink(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Inter
 
     let changed = false
     if (Unit.isAtomic(unit) && Link.isLoci(loci)) {
+        const groupCount = unit.links.edgeCount * 2
         for (const b of loci.links) {
-            const unitIdx = Unit.findUnitById(b.aUnit.id, group.units)
-            if (unitIdx !== -1) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
                 const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
                 if (idx !== -1) {
-                    if (apply(Interval.ofSingleton(idx))) changed = true
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
                 }
             }
         }
diff --git a/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
index 0ace3c5262d2d6ad41fe037b03a982400857c89f..8b5d893ef85dc1b6ae8b18ea8a651c3427e8872d 100644
--- a/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
+++ b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
@@ -9,14 +9,14 @@ import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getElementLoci, markElement, StructureElementIterator } from './util/element';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
-import { Segmentation, SortedArray } from 'mol-data/int';
+import { Segmentation } from 'mol-data/int';
 import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
 import { getElementIndexForAtomId, getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
 import { addCylinder } from '../../../mesh/builder/cylinder';
 import { Box } from '../../../primitive/box';
+import { NucleotideLocationIterator, markNucleotideElement, getNucleotideElementLoci } from './util/nucleotide';
 
 const p1 = Vec3.zero()
 const p2 = Vec3.zero()
@@ -82,25 +82,28 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, props:
                     idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
                 }
 
-                if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1 && idx5 !== -1 && idx6 !== -1) {
-                    pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); pos(idx5, p5); pos(idx6, p6)
-                    Vec3.normalize(v12, Vec3.sub(v12, p2, p1))
-                    Vec3.normalize(v34, Vec3.sub(v34, p4, p3))
-                    Vec3.normalize(vC, Vec3.cross(vC, v12, v34))
-                    Mat4.targetTo(t, p1, p2, vC)
-                    Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2)
-                    Mat4.scale(t, t, Vec3.set(sVec, width, depth, height))
-                    Mat4.setTranslation(t, center)
-                    builder.setGroup(SortedArray.findPredecessorIndex(elements, idx6))
-                    builder.add(t, box)
+                if (idx5 !== -1 && idx6 !== -1) {
+                    pos(idx5, p5); pos(idx6, p6)
+                    builder.setGroup(i)
                     addCylinder(builder, p5, p6, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+                    if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1) {
+                        pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4);
+                        Vec3.normalize(v12, Vec3.sub(v12, p2, p1))
+                        Vec3.normalize(v34, Vec3.sub(v34, p4, p3))
+                        Vec3.normalize(vC, Vec3.cross(vC, v12, v34))
+                        Mat4.targetTo(t, p1, p2, vC)
+                        Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2)
+                        Mat4.scale(t, t, Vec3.set(sVec, width, depth, height))
+                        Mat4.setTranslation(t, center)
+                        builder.add(t, box)
+                    }
                 }
-            }
 
-            if (i % 10000 === 0 && ctx.shouldUpdate) {
-                await ctx.update({ message: 'Nucleotide block mesh', current: i });
+                if (i % 10000 === 0 && ctx.shouldUpdate) {
+                    await ctx.update({ message: 'Nucleotide block mesh', current: i });
+                }
+                ++i
             }
-            ++i
         }
     }
 
@@ -116,9 +119,9 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> {
     return UnitsMeshVisual<NucleotideBlockProps>({
         defaultProps: DefaultNucleotideBlockProps,
         createMesh: createNucleotideBlockMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: NucleotideLocationIterator.fromGroup,
+        getLoci: getNucleotideElementLoci,
+        mark: markNucleotideElement,
         setUpdateState: () => {}
     })
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
index e3b1b00ae8c4923e157908bd9ecd404e10285b79..d975ae940228601a3fd1a833979d28dc2c0dc96e 100644
--- a/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
@@ -5,11 +5,11 @@
  */
 
 import { Unit } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer';
+import { PolymerBackboneIterator } from './util/polymer';
 import { getElementLoci, markElement, StructureElementIterator } from './util/element';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
@@ -24,7 +24,7 @@ export interface PolymerBackboneCylinderProps {
 }
 
 async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerBackboneCylinderProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -47,11 +47,11 @@ async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit
         pos(centerB.element, pB)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA)
-        builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerA.element))
+        builder.setGroup(OrderedSet.indexOf(elements, centerA.element))
         addCylinder(builder, pA, pB, 0.5, cylinderProps)
 
         cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB)
-        builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerB.element))
+        builder.setGroup(OrderedSet.indexOf(elements, centerB.element))
         addCylinder(builder, pB, pA, 0.5, cylinderProps)
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
@@ -73,9 +73,12 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneProps> {
     return UnitsMeshVisual<PolymerBackboneProps>({
         defaultProps: DefaultPolymerBackboneProps,
         createMesh: createPolymerBackboneCylinderMesh,
+        // TODO create a specialized location iterator
         createLocationIterator: StructureElementIterator.fromGroup,
         getLoci: getElementLoci,
         mark: markElement,
-        setUpdateState: () => {}
+        setUpdateState: (state: MeshUpdateState, newProps: PolymerBackboneProps, currentProps: PolymerBackboneProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts b/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
index 6ad043eba547f99896cfbc6e620c709b76dfcc7d..1e6b8e42a198d66249becde9ff3219b723a659ee 100644
--- a/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
+++ b/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
@@ -7,15 +7,13 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci, StructureElementIterator } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
 import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
-import { OrderedSet } from 'mol-data/int';
 import { Wedge } from '../../../primitive/wedge';
 
 const t = Mat4.identity()
@@ -35,7 +33,7 @@ export interface PolymerDirectionWedgeProps {
 }
 
 async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, props: PolymerDirectionWedgeProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -51,7 +49,7 @@ async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit,
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element))
+        builder.setGroup(i)
 
         const isNucleic = v.moleculeType === MoleculeType.DNA || v.moleculeType === MoleculeType.RNA
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -95,9 +93,9 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> {
     return UnitsMeshVisual<PolymerDirectionProps>({
         defaultProps: DefaultPolymerDirectionProps,
         createMesh: createPolymerDirectionWedgeMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: PolymerLocationIterator.fromGroup,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: () => {}
     })
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
index 047850c67aae193be0cb0035aed18925e054bfcd..24038dfc3182746c74d2ae0336a5556ff00cfcde 100644
--- a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
+++ b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
@@ -4,13 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, StructureElement } from 'mol-model/structure';
-import { UnitsVisual } from '..';
+import { Unit } from 'mol-model/structure';
+import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerGapCount, PolymerGapIterator } from './util/polymer';
-import { getElementLoci, markElement, StructureElementIterator } from './util/element';
+import { PolymerGapIterator, PolymerGapLocationIterator, markPolymerGapElement, getPolymerGapElementLoci } from './util/polymer';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
@@ -26,7 +25,7 @@ export interface PolymerGapCylinderProps {
 }
 
 async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerGapCylinderProps, mesh?: Mesh) {
-    const polymerGapCount = getPolymerGapCount(unit)
+    const polymerGapCount = unit.gapElements.length
     if (!polymerGapCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -35,11 +34,9 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro
     const vertexCountEstimate = segmentCount * radialSegments * 2 * polymerGapCount * 2
     const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh)
 
-    const { elements } = unit
     const pos = unit.conformation.invariantPosition
     const pA = Vec3.zero()
     const pB = Vec3.zero()
-    const l = StructureElement.create(unit)
     const cylinderProps: CylinderProps = {
         radiusTop: 1, radiusBottom: 1, topCap: true, bottomCap: true, radialSegments
     }
@@ -49,30 +46,26 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro
     while (polymerGapIt.hasNext) {
         const { centerA, centerB } = polymerGapIt.move()
         if (centerA.element === centerB.element) {
-            builder.setGroup(centerA.element)
-            pos(elements[centerA.element], pA)
+            builder.setGroup(i)
+            pos(centerA.element, pA)
             addSphere(builder, pA, 0.6, 0)
         } else {
-            const elmA = elements[centerA.element]
-            const elmB = elements[centerB.element]
-            pos(elmA, pA)
-            pos(elmB, pB)
+            pos(centerA.element, pA)
+            pos(centerB.element, pB)
 
-            l.element = elmA
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
-            builder.setGroup(centerA.element)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA)
+            builder.setGroup(i)
             addFixedCountDashedCylinder(builder, pA, pB, 0.5, segmentCount, cylinderProps)
 
-            l.element = elmB
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l)
-            builder.setGroup(centerB.element)
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB)
+            builder.setGroup(i + 1)
             addFixedCountDashedCylinder(builder, pB, pA, 0.5, segmentCount, cylinderProps)
         }
 
         if (i % 10000 === 0 && ctx.shouldUpdate) {
             await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount });
         }
-        ++i
+        i += 2
     }
 
     return builder.getMesh()
@@ -88,9 +81,11 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> {
     return UnitsMeshVisual<PolymerGapProps>({
         defaultProps: DefaultPolymerGapProps,
         createMesh: createPolymerGapCylinderMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
-        setUpdateState: () => {}
+        createLocationIterator: PolymerGapLocationIterator.fromGroup,
+        getLoci: getPolymerGapElementLoci,
+        mark: markPolymerGapElement,
+        setUpdateState: (state: MeshUpdateState, newProps: PolymerGapProps, currentProps: PolymerGapProps) => {
+            state.createMesh = newProps.radialSegments !== currentProps.radialSegments
+        }
     })
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
index f65c7fb9cb36488df559bbe8a412d1ee03bd81f6..1a37e4d9809fa189a779c2dcb2b969ccc382dcba 100644
--- a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
+++ b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
@@ -7,14 +7,12 @@
 import { Unit } from 'mol-model/structure';
 import { UnitsVisual, MeshUpdateState } from '..';
 import { RuntimeContext } from 'mol-task'
-import { markElement, getElementLoci, StructureElementIterator } from './util/element';
 import { Mesh } from '../../../mesh/mesh';
 import { MeshBuilder } from '../../../mesh/mesh-builder';
-import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer';
 import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types';
 import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
-import { OrderedSet } from 'mol-data/int';
 import { addSheet } from '../../../mesh/builder/sheet';
 import { addTube } from '../../../mesh/builder/tube';
 
@@ -29,7 +27,7 @@ export interface PolymerTraceMeshProps {
 // TODO handle polymer ends properly
 
 async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: PolymerTraceMeshProps, mesh?: Mesh) {
-    const polymerElementCount = getPolymerElementCount(unit)
+    const polymerElementCount = unit.polymerElements.length
     if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
     const sizeTheme = SizeTheme(props.sizeTheme)
@@ -45,7 +43,7 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: Po
     const polymerTraceIt = PolymerTraceIterator(unit)
     while (polymerTraceIt.hasNext) {
         const v = polymerTraceIt.move()
-        builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element))
+        builder.setGroup(i)
 
         const isNucleicType = isNucleic(v.moleculeType)
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
@@ -95,9 +93,9 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> {
     return UnitsMeshVisual<PolymerTraceProps>({
         defaultProps: DefaultPolymerTraceProps,
         createMesh: createPolymerTraceMesh,
-        createLocationIterator: StructureElementIterator.fromGroup,
-        getLoci: getElementLoci,
-        mark: markElement,
+        createLocationIterator: PolymerLocationIterator.fromGroup,
+        getLoci: getPolymerElementLoci,
+        mark: markPolymerElement,
         setUpdateState: (state: MeshUpdateState, newProps: PolymerTraceProps, currentProps: PolymerTraceProps) => {
             state.createMesh = (
                 newProps.linearSegments !== currentProps.linearSegments ||
diff --git a/src/mol-geo/representation/structure/visual/util/common.ts b/src/mol-geo/representation/structure/visual/util/common.ts
index e069563c8771d3a7d6281a4f91720be24a3b6c27..0dfb1317bd369f48ba0ec9f8590f0811f3cdba6b 100644
--- a/src/mol-geo/representation/structure/visual/util/common.ts
+++ b/src/mol-geo/representation/structure/visual/util/common.ts
@@ -6,57 +6,57 @@
  */
 
 import { Unit, Structure } from 'mol-model/structure';
-import { Mat4 } from 'mol-math/linear-algebra'
-
-import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor } from '../../../../util/color-data';
-import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize } from '../../../../util/size-data';
+import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor, ColorType } from '../../../../util/color-data';
+import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize, SizeType } from '../../../../util/size-data';
 import { ValueCell } from 'mol-util';
 import { LocationIterator } from '../../../../util/location-iterator';
 import { Mesh } from '../../../../mesh/mesh';
-import { MeshValues } from 'mol-gl/renderable';
+import { MeshValues, PointValues } from 'mol-gl/renderable';
 import { getMeshData } from '../../../../util/mesh-data';
-import { MeshProps, createMeshValues, createRenderableState, createIdentityTransform } from '../../../util';
+import { MeshProps, createMeshValues, createRenderableState, createPointValues } from '../../../util';
 import { StructureProps } from '../..';
 import { createMarkers } from '../../../../util/marker-data';
-import { createMeshRenderObject } from 'mol-gl/render-object';
+import { createMeshRenderObject, createPointRenderObject } from 'mol-gl/render-object';
 import { ColorThemeProps, ColorTheme } from 'mol-view/theme/color';
 import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size';
+import { RuntimeContext } from 'mol-task';
+import { PointProps } from 'mol-geo/representation/structure/representation/point';
+import { fillSerial } from 'mol-util/array';
+import { TransformData, createIdentityTransform, createTransforms } from '../../../../util/transform-data';
 
-export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
-    const unitCount = units.length
-    const n = unitCount * 16
-    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
-    for (let i = 0; i < unitCount; i++) {
-        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
-    }
-    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
+function getGranularity(locationIt: LocationIterator, granularity: ColorType | SizeType) {
+    // Always use 'group' granularity for 'complex' location iterators,
+    // i.e. for which an instance may include multiple units
+    return granularity === 'instance' && locationIt.isComplex ? 'group' : granularity
 }
 
-export function createColors(locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData) {
+export function createColors(ctx: RuntimeContext, locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData): Promise<ColorData> {
     const colorTheme = ColorTheme(props)
-    switch (colorTheme.kind) {
-        case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData)
-        case 'group': return createGroupColor(locationIt, colorTheme.color, colorData)
-        case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData)
-        case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData)
+    switch (getGranularity(locationIt, colorTheme.granularity)) {
+        case 'uniform': return createUniformColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'group': return createGroupColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'groupInstance': return createGroupInstanceColor(ctx, locationIt, colorTheme.color, colorData)
+        case 'instance': return createInstanceColor(ctx, locationIt, colorTheme.color, colorData)
     }
 }
 
-export function createSizes(locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): SizeData {
+export async function createSizes(ctx: RuntimeContext, locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): Promise<SizeData> {
     const sizeTheme = SizeTheme(props)
-    switch (sizeTheme.kind) {
-        case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData)
-        case 'group': return createGroupSize(locationIt, sizeTheme.size, sizeData)
-        case 'groupInstance': return createGroupInstanceSize(locationIt, sizeTheme.size, sizeData)
-        case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData)
+    switch (getGranularity(locationIt, sizeTheme.granularity)) {
+        case 'uniform': return createUniformSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'group': return createGroupSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'groupInstance': return createGroupInstanceSize(ctx, locationIt, sizeTheme.size, sizeData)
+        case 'instance': return createInstanceSize(ctx, locationIt, sizeTheme.size, sizeData)
     }
 }
 
+// mesh
+
 type StructureMeshProps = Required<MeshProps & StructureProps>
 
-function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+async function _createMeshValues(ctx: RuntimeContext, transforms: TransformData, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const { instanceCount, groupCount } = locationIt
-    const color = createColors(locationIt, props.colorTheme)
+    const color = await createColors(ctx, locationIt, props.colorTheme)
     const marker = createMarkers(instanceCount * groupCount)
 
     const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
@@ -65,35 +65,69 @@ function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, loca
         ...getMeshData(mesh),
         ...color,
         ...marker,
-        aTransform: transforms,
+        ...transforms,
         elements: mesh.indexBuffer,
         ...createMeshValues(props, counts)
     }
 }
 
-export function createComplexMeshValues(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function createComplexMeshValues(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createIdentityTransform()
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
 }
 
-export function createUnitsMeshValues(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function createUnitsMeshValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createTransforms(group)
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
 }
 
-export function createComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
-    const values = createComplexMeshValues(structure, mesh, locationIt, props)
+export async function createComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = await createComplexMeshValues(ctx, structure, mesh, locationIt, props)
     const state = createRenderableState(props)
     return createMeshRenderObject(values, state)
 }
 
-export function createUnitsMeshRenderObject(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
-    const values = createUnitsMeshValues(group, mesh, locationIt, props)
+export async function createUnitsMeshRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) {
+    const values = await createUnitsMeshValues(ctx, group, mesh, locationIt, props)
     const state = createRenderableState(props)
     return createMeshRenderObject(values, state)
 }
 
-export function updateComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues {
+export async function updateComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> {
     const transforms = createIdentityTransform()
-    return _createMeshValues(transforms, mesh, locationIt, props)
+    return _createMeshValues(ctx, transforms, mesh, locationIt, props)
+}
+
+// point
+
+type StructurePointProps = Required<PointProps & StructureProps>
+
+async function _createPointValues(ctx: RuntimeContext, transforms: TransformData, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> {
+    const { instanceCount, groupCount } = locationIt
+    const color = await createColors(ctx, locationIt, props.colorTheme)
+    const size = await createSizes(ctx, locationIt, props.sizeTheme)
+    const marker = createMarkers(instanceCount * groupCount)
+
+    const counts = { drawCount: groupCount, groupCount, instanceCount }
+
+    return {
+        aPosition: vertices,
+        aGroup: ValueCell.create(fillSerial(new Float32Array(groupCount))),
+        ...color,
+        ...size,
+        ...marker,
+        ...transforms,
+        ...createPointValues(props, counts)
+    }
+}
+
+export async function createUnitsPointValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> {
+    const transforms = createTransforms(group)
+    return _createPointValues(ctx, transforms, vertices, locationIt, props)
+}
+
+export async function createUnitsPointRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps) {
+    const values = await createUnitsPointValues(ctx, group, vertices, locationIt, props)
+    const state = createRenderableState(props)
+    return createPointRenderObject(values, state)
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/util/element.ts b/src/mol-geo/representation/structure/visual/util/element.ts
index d469d6f2f53d63d0233fcf6c3defd0122c4483dc..0de1861eefddaeac2d28b8d6e8f20790f4eca963 100644
--- a/src/mol-geo/representation/structure/visual/util/element.ts
+++ b/src/mol-geo/representation/structure/visual/util/element.ts
@@ -57,8 +57,8 @@ export function markElement(loci: Loci, group: Unit.SymmetryGroup, apply: (inter
     let changed = false
     if (StructureElement.isLoci(loci)) {
         for (const e of loci.elements) {
-            const unitIdx = Unit.findUnitById(e.unit.id, group.units)
-            if (unitIdx !== -1) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
                 if (Interval.is(e.indices)) {
                     const start = unitIdx * elementCount + Interval.start(e.indices);
                     const end = unitIdx * elementCount + Interval.end(e.indices);
@@ -87,11 +87,12 @@ export function getElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup,
 
 export namespace StructureElementIterator {
     export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
-        const unit = group.units[0]
         const groupCount = group.elements.length
         const instanceCount = group.units.length
-        const location = StructureElement.create(unit)
-        const getLocation = (groupIndex: number) => {
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
             location.element = unit.elements[groupIndex]
             return location
         }
diff --git a/src/mol-geo/representation/structure/visual/util/link.ts b/src/mol-geo/representation/structure/visual/util/link.ts
index 865e52d99668e5d38d9c950928953049444e2253..ca73e55b9cb017173c7cb9af15701e7aff21a1fc 100644
--- a/src/mol-geo/representation/structure/visual/util/link.ts
+++ b/src/mol-geo/representation/structure/visual/util/link.ts
@@ -126,8 +126,10 @@ export namespace LinkIterator {
         const unit = group.units[0]
         const groupCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
         const instanceCount = group.units.length
-        const location = StructureElement.create(unit)
-        const getLocation = (groupIndex: number) => {
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
             location.element = unit.elements[(unit as Unit.Atomic).links.a[groupIndex]]
             return location
         }
@@ -146,6 +148,6 @@ export namespace LinkIterator {
             location.bIndex = bond.indexB as StructureElement.UnitIndex
             return location
         }
-        return LocationIterator(groupCount, instanceCount, getLocation)
+        return LocationIterator(groupCount, instanceCount, getLocation, true)
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/util/nucleotide.ts b/src/mol-geo/representation/structure/visual/util/nucleotide.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d62fae0e165047165c9d0266c5c4c8a8735fa18b
--- /dev/null
+++ b/src/mol-geo/representation/structure/visual/util/nucleotide.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, StructureElement } from 'mol-model/structure';
+import { LocationIterator } from '../../../../util/location-iterator';
+import { getNucleotideElements } from 'mol-model/structure/structure/util/nucleotide';
+import { PickingId } from '../../../../util/picking';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { OrderedSet, Interval } from 'mol-data/int';
+
+export namespace NucleotideLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const u = group.units[0]
+        const nucleotideElementIndices = Unit.isAtomic(u) ? getNucleotideElements(u) : []
+        const groupCount = nucleotideElementIndices.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = nucleotideElementIndices[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export function getNucleotideElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        if (Unit.isAtomic(unit)) {
+            const unitIndex = OrderedSet.indexOf(unit.elements, unit.nucleotideElements[groupId]) as StructureElement.UnitIndex
+            if (unitIndex !== -1) {
+                const indices = OrderedSet.ofSingleton(unitIndex)
+                return StructureElement.Loci([{ unit, indices }])
+            }
+        }
+    }
+    return EmptyLoci
+}
+
+export function markNucleotideElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    const u = group.units[0]
+    if (StructureElement.isLoci(loci) && Unit.isAtomic(u)) {
+        const groupCount = u.nucleotideElements.length
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined && Unit.isAtomic(e.unit)) {
+                if (Interval.is(e.indices)) {
+                    const min = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.min(e.indices)])
+                    const max = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.max(e.indices)])
+                    if (min !== -1 && max !== -1) {
+                        if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true
+                    }
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[e.indices[i]])
+                        if (idx !== -1) {
+                            if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                        }
+                    }
+                }
+            }
+        }
+    }
+    return changed
+}
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/util/polymer.ts b/src/mol-geo/representation/structure/visual/util/polymer.ts
index 24d9b7e937e2953c1e1acb3c958a6f69de6b264c..8586b9e50ad5188b6ab1d7a0b37e3d70944a112d 100644
--- a/src/mol-geo/representation/structure/visual/util/polymer.ts
+++ b/src/mol-geo/representation/structure/visual/util/polymer.ts
@@ -4,9 +4,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, ElementIndex } from 'mol-model/structure';
-import { Segmentation, OrderedSet, Interval } from 'mol-data/int';
+import { Unit, ElementIndex, StructureElement, Link } from 'mol-model/structure';
 import SortedRanges from 'mol-data/int/sorted-ranges';
+import { LocationIterator } from '../../../../util/location-iterator';
+import { PickingId } from '../../../../util/picking';
+import { OrderedSet, Interval } from 'mol-data/int';
+import { EmptyLoci, Loci } from 'mol-model/loci';
 
 export * from './polymer/backbone-iterator'
 export * from './polymer/gap-iterator'
@@ -29,41 +32,106 @@ export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
     }
 }
 
-export function getPolymerElementCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements)
-    switch (unit.kind) {
-        case Unit.Kind.Atomic:
-            const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements)
-            while (polymerIt.hasNext) {
-                const polymerSegment = polymerIt.move()
-                residueIt.setSegment(polymerSegment)
-                while (residueIt.hasNext) {
-                    const residueSegment = residueIt.move()
-                    const { start, end } = residueSegment
-                    if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+export namespace PolymerLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const polymerElements = group.units[0].polymerElements
+        const groupCount = polymerElements.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = polymerElements[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export namespace PolymerGapLocationIterator {
+    export function fromGroup(group: Unit.SymmetryGroup): LocationIterator {
+        const gapElements = group.units[0].gapElements
+        const groupCount = gapElements.length
+        const instanceCount = group.units.length
+        const location = StructureElement.create()
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const unit = group.units[instanceIndex]
+            location.unit = unit
+            location.element = gapElements[groupIndex]
+            return location
+        }
+        return LocationIterator(groupCount, instanceCount, getLocation)
+    }
+}
+
+export function getPolymerElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const unitIndex = OrderedSet.indexOf(unit.elements, unit.polymerElements[groupId]) as StructureElement.UnitIndex
+        if (unitIndex !== -1) {
+            const indices = OrderedSet.ofSingleton(unitIndex)
+            return StructureElement.Loci([{ unit, indices }])
+        }
+    }
+    return EmptyLoci
+}
+
+export function markPolymerElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    const groupCount = group.units[0].polymerElements.length
+
+    let changed = false
+    if (StructureElement.isLoci(loci)) {
+        for (const e of loci.elements) {
+            const unitIdx = group.unitIndexMap.get(e.unit.id)
+            if (unitIdx !== undefined) {
+                if (Interval.is(e.indices)) {
+                    const min =  + OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.min(e.indices)])
+                    const max = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.max(e.indices)])
+                    if (min !== -1 && max !== -1) {
+                        if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true
+                    }
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[e.indices[i]])
+                        if (idx !== -1) {
+                            if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true
+                        }
+                    }
                 }
             }
-            break
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            while (polymerIt.hasNext) {
-                const { start, end } = polymerIt.move()
-                count += OrderedSet.intersectionSize(Interval.ofBounds(elements[start], elements[end - 1]), elements)
-            }
-            break
+        }
     }
-    return count
+    return changed
 }
 
-export function getPolymerGapCount(unit: Unit) {
-    let count = 0
-    const { elements } = unit
-    const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements)
-    while (gapIt.hasNext) {
-        const { start, end } = gapIt.move()
-        if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+export function getPolymerGapElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, groupId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const unitIndexA = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId]) as StructureElement.UnitIndex
+        const unitIndexB = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId % 2 ? groupId - 1 : groupId + 1]) as StructureElement.UnitIndex
+        if (unitIndexA !== -1 && unitIndexB !== -1) {
+            return Link.Loci([ Link.Location(unit, unitIndexA, unit, unitIndexB) ])
+        }
     }
-    return count
+    return EmptyLoci
 }
+
+export function markPolymerGapElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) {
+    let changed = false
+    if (Link.isLoci(loci)) {
+        const groupCount = group.units[0].gapElements.length
+        for (const b of loci.links) {
+            const unitIdx = group.unitIndexMap.get(b.aUnit.id)
+            if (unitIdx !== undefined) {
+                const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex])
+                const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex])
+                if (idxA !== -1 && idxB !== -1) {
+                    if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true
+                }
+            }
+        }
+    }
+    return changed
+}
\ No newline at end of file
diff --git a/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts
index eaa3c029c7ab1455a093701b7b7b3bc3fb582da1..31170313fb9d345db914146b927b1650c2a38a14 100644
--- a/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts
+++ b/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts
@@ -10,6 +10,7 @@ import Iterator from 'mol-data/iterator';
 import SortedRanges from 'mol-data/int/sorted-ranges';
 import { getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getPolymerRanges } from '../polymer';
+import { AtomRole } from 'mol-model/structure/model/types';
 
 /** Iterates over consecutive pairs of residues/coarse elements in polymers */
 export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePair> {
@@ -43,13 +44,19 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
     private residueSegment: Segmentation.Segment<ResidueIndex>
     hasNext: boolean = false;
 
+    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
+        const { atomicHierarchy } = this.unit.model
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
+    }
+
     move() {
         if (this.state === AtomicPolymerBackboneIteratorState.nextPolymer) {
             while (this.polymerIt.hasNext) {
                 this.residueIt.setSegment(this.polymerIt.move());
                 if (this.residueIt.hasNext) {
                     this.residueSegment = this.residueIt.move()
-                    this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace')
+                    this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
                     this.state = AtomicPolymerBackboneIteratorState.nextResidue
                     break
                 }
@@ -59,7 +66,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
         if (this.state === AtomicPolymerBackboneIteratorState.nextResidue) {
             this.residueSegment = this.residueIt.move()
             this.value.centerA.element = this.value.centerB.element
-            this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace')
+            this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
             if (!this.residueIt.hasNext) {
                 if (this.unit.model.atomicHierarchy.cyclicPolymerMap.has(this.residueSegment.index)) {
                     this.state = AtomicPolymerBackboneIteratorState.cycle
@@ -71,7 +78,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa
         } else if (this.state === AtomicPolymerBackboneIteratorState.cycle) {
             const { cyclicPolymerMap } = this.unit.model.atomicHierarchy
             this.value.centerA.element = this.value.centerB.element
-            this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
+            this.value.centerB.element = this.getElementIndex(cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
             // TODO need to advance to a polymer that has two or more residues (can't assume it has)
             this.state = AtomicPolymerBackboneIteratorState.nextPolymer
         }
diff --git a/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts
index 9b91f2894e01ebd89546cee4fafa59514248ee4d..663f10162596a611ff62efc0c4811a5508e99255 100644
--- a/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts
+++ b/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts
@@ -5,7 +5,6 @@
  */
 
 import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/structure';
-import { SortedArray } from 'mol-data/int';
 import { AtomRole } from 'mol-model/structure/model/types';
 import Iterator from 'mol-data/iterator';
 import SortedRanges from 'mol-data/int/sorted-ranges';
@@ -40,13 +39,8 @@ export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> {
     hasNext: boolean = false;
 
     private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
-        const index = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
-        // TODO handle case when it returns -1
-        const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex
-        if (elementIndex === -1) {
-            console.log('-1', residueIndex, atomRole, index)
-        }
-        return elementIndex === -1 ? 0 as ElementIndex : elementIndex
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? this.unit.model.atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
     }
 
     move() {
diff --git a/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts
index df4cfabc5ae57639e16fed87086ab7af9f5f78ec..e4b98c009cfe449ea2bd183a5280b6c564fcff9c 100644
--- a/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts
+++ b/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts
@@ -89,24 +89,25 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
         this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]]
     }
 
-    private getAtomIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
-        const { cyclicPolymerMap } = this.unit.model.atomicHierarchy
+    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
+        const { atomicHierarchy } = this.unit.model
         if (residueIndex < this.residueSegmentMin) {
-            const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMin)
+            const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMin)
             if (cyclicIndex !== undefined) {
                 residueIndex = cyclicIndex - (this.residueSegmentMin - residueIndex - 1) as ResidueIndex
             } else {
                 residueIndex = this.residueSegmentMin
             }
         } else if (residueIndex > this.residueSegmentMax) {
-            const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMax)
+            const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMax)
             if (cyclicIndex !== undefined) {
                 residueIndex = cyclicIndex + (residueIndex - this.residueSegmentMax - 1) as ResidueIndex
             } else {
                 residueIndex = this.residueSegmentMax
             }
         }
-        return getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex
     }
 
     private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) {
@@ -135,19 +136,19 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
         if (this.state === AtomicPolymerTraceIteratorState.nextResidue) {
             const { index: residueIndex } = residueIt.move();
-            value.center.element = this.getAtomIndex(residueIndex, 'trace')
+            value.center.element = this.getElementIndex(residueIndex, 'trace')
 
-            this.pos(this.p0, this.getAtomIndex(residueIndex - 3 as ResidueIndex, 'trace'))
-            this.pos(this.p1, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'trace'))
-            this.pos(this.p2, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'trace'))
-            this.pos(this.p3, this.getAtomIndex(residueIndex, 'trace'))
-            this.pos(this.p4, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'trace'))
-            this.pos(this.p5, this.getAtomIndex(residueIndex + 2 as ResidueIndex, 'trace'))
-            this.pos(this.p6, this.getAtomIndex(residueIndex + 3 as ResidueIndex, 'trace'))
+            this.pos(this.p0, this.getElementIndex(residueIndex - 3 as ResidueIndex, 'trace'))
+            this.pos(this.p1, this.getElementIndex(residueIndex - 2 as ResidueIndex, 'trace'))
+            this.pos(this.p2, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'trace'))
+            this.pos(this.p3, this.getElementIndex(residueIndex, 'trace'))
+            this.pos(this.p4, this.getElementIndex(residueIndex + 1 as ResidueIndex, 'trace'))
+            this.pos(this.p5, this.getElementIndex(residueIndex + 2 as ResidueIndex, 'trace'))
+            this.pos(this.p6, this.getElementIndex(residueIndex + 3 as ResidueIndex, 'trace'))
 
             // this.pos(this.v01, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'direction'))
-            this.pos(this.v12, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'direction'))
-            this.pos(this.v23, this.getAtomIndex(residueIndex, 'direction'))
+            this.pos(this.v12, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'direction'))
+            this.pos(this.v23, this.getElementIndex(residueIndex, 'direction'))
             // this.pos(this.v34, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'direction'))
 
             this.value.secStrucType = this.unit.model.properties.secondaryStructure.type[residueIndex]
diff --git a/src/mol-geo/representation/util.ts b/src/mol-geo/representation/util.ts
index 4edca150e52f42e76d67a5b28fd3f76754d4ed19..a7fdea3c87c5a3dab8addee34d4b16257e39987c 100644
--- a/src/mol-geo/representation/util.ts
+++ b/src/mol-geo/representation/util.ts
@@ -9,14 +9,12 @@ import { BaseValues } from 'mol-gl/renderable/schema';
 import { MeshValues, RenderableState } from 'mol-gl/renderable';
 import { defaults } from 'mol-util';
 import { Structure } from 'mol-model/structure';
-import { fillSerial } from 'mol-util/array';
-import { Mat4 } from 'mol-math/linear-algebra';
 
 export const DefaultBaseProps = {
     alpha: 1,
     visible: true,
     depthMask: true,
-    useFog: true,
+    useFog: false,
     quality: 'auto' as VisualQuality
 }
 export type BaseProps = typeof DefaultBaseProps
@@ -29,22 +27,20 @@ export const DefaultMeshProps = {
 }
 export type MeshProps = typeof DefaultMeshProps
 
-const identityTransform = new Float32Array(16)
-Mat4.toArray(Mat4.identity(), identityTransform, 0)
-export function createIdentityTransform(transforms?: ValueCell<Float32Array>) {
-    return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform)
+export const DefaultPointProps = {
+    ...DefaultBaseProps,
+    pointSizeAttenuation: true
 }
+export type PointProps = typeof DefaultPointProps
 
 type Counts = { drawCount: number, groupCount: number, instanceCount: number }
 
 export function createBaseValues(props: Required<BaseProps>, counts: Counts) {
     return {
         uAlpha: ValueCell.create(props.alpha),
-        uInstanceCount: ValueCell.create(counts.instanceCount),
         uGroupCount: ValueCell.create(counts.groupCount),
-        aInstance: ValueCell.create(fillSerial(new Float32Array(counts.instanceCount))),
         drawCount: ValueCell.create(counts.drawCount),
-        instanceCount: ValueCell.create(counts.instanceCount),
+        dUseFog: ValueCell.create(props.useFog),
     }
 }
 
@@ -54,7 +50,13 @@ export function createMeshValues(props: Required<MeshProps>, counts: Counts) {
         dDoubleSided: ValueCell.create(props.doubleSided),
         dFlatShaded: ValueCell.create(props.flatShaded),
         dFlipSided: ValueCell.create(props.flipSided),
-        dUseFog: ValueCell.create(props.useFog),
+    }
+}
+
+export function createPointValues(props: Required<PointProps>, counts: Counts) {
+    return {
+        ...createBaseValues(props, counts),
+        dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
     }
 }
 
@@ -67,6 +69,7 @@ export function createRenderableState(props: Required<BaseProps>): RenderableSta
 
 export function updateBaseValues(values: BaseValues, props: Required<BaseProps>) {
     ValueCell.updateIfChanged(values.uAlpha, props.alpha)
+    ValueCell.updateIfChanged(values.dUseFog, props.useFog)
 }
 
 export function updateMeshValues(values: MeshValues, props: Required<MeshProps>) {
@@ -74,7 +77,6 @@ export function updateMeshValues(values: MeshValues, props: Required<MeshProps>)
     ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided)
     ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded)
     ValueCell.updateIfChanged(values.dFlipSided, props.flipSided)
-    ValueCell.updateIfChanged(values.dUseFog, props.useFog)
 }
 
 export function updateRenderableState(state: RenderableState, props: Required<BaseProps>) {
@@ -82,20 +84,32 @@ export function updateRenderableState(state: RenderableState, props: Required<Ba
     state.depthMask = props.depthMask
 }
 
-export type VisualQuality = 'custom' | 'auto' | 'highest' | 'high' | 'medium' | 'low' | 'lowest'
+export const VisualQualityInfo = {
+    'custom': {},
+    'auto': {},
+    'highest': {},
+    'high': {},
+    'medium': {},
+    'low': {},
+    'lowest': {},
+}
+export type VisualQuality = keyof typeof VisualQualityInfo
+export const VisualQualityNames = Object.keys(VisualQualityInfo)
 
-interface QualityProps {
+export interface QualityProps {
     quality: VisualQuality
     detail: number
     radialSegments: number
+    linearSegments: number
 }
 
-export function getQualityProps(props: Partial<QualityProps>, structure: Structure) {
+export function getQualityProps(props: Partial<QualityProps>, structure?: Structure) {
     let quality = defaults(props.quality, 'auto' as VisualQuality)
-    let detail = 1
-    let radialSegments = 12
+    let detail = defaults(props.detail, 1)
+    let radialSegments = defaults(props.radialSegments, 12)
+    let linearSegments = defaults(props.linearSegments, 8)
 
-    if (quality === 'auto') {
+    if (quality === 'auto' && structure) {
         const score = structure.elementCount
         if (score > 500_000) {
             quality = 'lowest'
@@ -110,33 +124,38 @@ export function getQualityProps(props: Partial<QualityProps>, structure: Structu
 
     switch (quality) {
         case 'highest':
-            detail = 2
+            detail = 3
             radialSegments = 36
+            linearSegments = 18
             break
         case 'high':
-            detail = 1
+            detail = 2
             radialSegments = 24
+            linearSegments = 12
             break
         case 'medium':
-            detail = 0
+            detail = 1
             radialSegments = 12
+            linearSegments = 8
             break
         case 'low':
             detail = 0
             radialSegments = 5
+            linearSegments = 3
             break
         case 'lowest':
             detail = 0
             radialSegments = 3
+            linearSegments = 2
             break
         case 'custom':
-            detail = defaults(props.detail, 1)
-            radialSegments = defaults(props.radialSegments, 12)
+            // use defaults or given props as set above
             break
     }
 
     return {
         detail,
-        radialSegments
+        radialSegments,
+        linearSegments
     }
 }
\ No newline at end of file
diff --git a/src/mol-geo/representation/volume/index.ts b/src/mol-geo/representation/volume/index.ts
index 75555e2ca1ac77678a46c18ca5ffd9f89c9a3d09..9197885d3c9cebb758d2cd37218437ec3164f561 100644
--- a/src/mol-geo/representation/volume/index.ts
+++ b/src/mol-geo/representation/volume/index.ts
@@ -27,31 +27,32 @@ export function VolumeRepresentation<P extends VolumeProps>(visualCtor: (volumeD
     let _volumeData: VolumeData
     let _props: P
 
-    function create(volumeData: VolumeData, props: Partial<P> = {}) {
+    function createOrUpdate(props: Partial<P> = {}, volumeData?: VolumeData) {
         _props = Object.assign({}, DefaultVolumeProps, _props, props)
         return Task.create('VolumeRepresentation.create', async ctx => {
-            _volumeData = volumeData
-            const visual = visualCtor(_volumeData)
-            await visual.create(ctx, _volumeData, props)
-            renderObjects.push(visual.renderObject)
+            if (volumeData) {
+                _volumeData = volumeData
+                const visual = visualCtor(_volumeData)
+                await visual.createOrUpdate(ctx, props, _volumeData)
+                if (visual.renderObject) renderObjects.push(visual.renderObject)
+            } else {
+                throw new Error('missing volumeData')
+            }
         });
     }
 
-    function update(props: Partial<P>) {
-        return Task.create('VolumeRepresentation.update', async ctx => {})
-    }
-
     return {
+        label: 'Volume mesh',
         get renderObjects () { return renderObjects },
         get props () { return _props },
-        create,
-        update,
+        createOrUpdate,
         getLoci(pickingId: PickingId) {
             // TODO
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
             // TODO
+            return false
         },
         destroy() {
             // TODO
diff --git a/src/mol-geo/representation/volume/surface.ts b/src/mol-geo/representation/volume/surface.ts
index 1d289273d6e5783431b9185c98c8d09c3d1778ae..46671107fcfff72a825598c02692718e6c1953b7 100644
--- a/src/mol-geo/representation/volume/surface.ts
+++ b/src/mol-geo/representation/volume/surface.ts
@@ -57,9 +57,11 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
 
     return {
         get renderObject () { return renderObject },
-        async create(ctx: RuntimeContext, volume: VolumeData, props: SurfaceProps = {}) {
+        async createOrUpdate(ctx: RuntimeContext, props: SurfaceProps = {}, volume?: VolumeData) {
             props = { ...DefaultSurfaceProps, ...props }
 
+            if (!volume) return
+
             const mesh = await computeVolumeSurface(volume, curProps.isoValue).runAsChild(ctx)
             if (!props.flatShaded) {
                 Mesh.computeNormalsImmediate(mesh)
@@ -97,16 +99,13 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> {
 
             renderObject = createMeshRenderObject(values, state)
         },
-        async update(ctx: RuntimeContext, props: SurfaceProps) {
-            // TODO
-            return false
-        },
         getLoci(pickingId: PickingId) {
             // TODO
             return EmptyLoci
         },
         mark(loci: Loci, action: MarkerAction) {
             // TODO
+            return false
         },
         destroy() {
             // TODO
diff --git a/src/mol-geo/util/color-data.ts b/src/mol-geo/util/color-data.ts
index d8f0c3d2e72397637ce8d6064e6d834e31dfeb8b..e47eef2d8da951efe7169f051df225828dbc83bb 100644
--- a/src/mol-geo/util/color-data.ts
+++ b/src/mol-geo/util/color-data.ts
@@ -9,7 +9,9 @@ import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
 import { Color } from 'mol-util/color';
 import { Vec2, Vec3 } from 'mol-math/linear-algebra';
 import { LocationIterator } from './location-iterator';
-import { Location, NullLocation } from 'mol-model/location';
+import { NullLocation } from 'mol-model/location';
+import { LocationColor } from 'mol-view/theme/color';
+import { RuntimeContext } from 'mol-task';
 
 export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance'
 
@@ -21,8 +23,6 @@ export type ColorData = {
     dColorType: ValueCell<string>,
 }
 
-export type LocationColor = (location: Location, isSecondary: boolean) => Color
-
 const emptyColorTexture = { array: new Uint8Array(3), width: 1, height: 1 }
 function createEmptyColorTexture() {
     return {
@@ -49,8 +49,8 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
 }
 
 /** Creates color uniform */
-export function createUniformColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
-    return createValueColor(colorFn(NullLocation, false), colorData)
+export async function createUniformColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
+    return createValueColor(color(NullLocation, false), colorData)
 }
 
 export function createTextureColor(colors: TextureImage, type: ColorType, colorData?: ColorData): ColorData {
@@ -73,36 +73,51 @@ export function createTextureColor(colors: TextureImage, type: ColorType, colorD
 }
 
 /** Creates color texture with color for each instance/unit */
-export function createInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { instanceCount} = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= instanceCount * 3 ? colorData.tColor.ref.value : createTextureImage(instanceCount, 3)
-    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+    let i = 0
+    while (locationIt.hasNext) {
         const { location, isSecondary, instanceIndex } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, instanceIndex * 3)
+        Color.toArray(color(location, isSecondary), colors.array, instanceIndex * 3)
         locationIt.skipInstance()
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating instance colors', current: i, max: instanceCount });
+        }
+        ++i
     }
     return createTextureColor(colors, 'instance', colorData)
 }
 
 /** Creates color texture with color for each group (i.e. shared across instances/units) */
-export function createGroupColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createGroupColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { groupCount } = locationIt
     const colors = colorData && colorData.tColor.ref.value.array.length >= groupCount * 3 ? colorData.tColor.ref.value : createTextureImage(groupCount, 3)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const { location, isSecondary, groupIndex } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, groupIndex * 3)
+        Color.toArray(color(location, isSecondary), colors.array, groupIndex * 3)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group colors', current: i, max: groupCount });
+        }
+        ++i
     }
     return createTextureColor(colors, 'group', colorData)
 }
 
 /** Creates color texture with color for each group in each instance (i.e. for each unit) */
-export function createGroupInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData {
+export async function createGroupInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
     const colors = colorData && colorData.tColor.ref.value.array.length >= count * 3 ? colorData.tColor.ref.value : createTextureImage(count, 3)
-    while (locationIt.hasNext && !locationIt.isNextNewInstance) {
+    let i = 0
+    while (locationIt.hasNext) {
         const { location, isSecondary, index } = locationIt.move()
-        Color.toArray(colorFn(location, isSecondary), colors.array, index * 3)
+        Color.toArray(color(location, isSecondary), colors.array, index * 3)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group instance colors', current: i, max: count });
+        }
+        ++i
     }
     return createTextureColor(colors, 'groupInstance', colorData)
 }
\ No newline at end of file
diff --git a/src/mol-geo/util/location-iterator.ts b/src/mol-geo/util/location-iterator.ts
index 435d0c45c3be8181456098ec99331c4cf7f579d5..16c3517098b273acfd6fc7c48f58055fd07b335e 100644
--- a/src/mol-geo/util/location-iterator.ts
+++ b/src/mol-geo/util/location-iterator.ts
@@ -28,6 +28,8 @@ export interface LocationIterator extends Iterator<LocationValue> {
     readonly isNextNewInstance: boolean
     readonly groupCount: number
     readonly instanceCount: number
+    /** If true, may have multiple units per instance; if false one unit per instance */
+    readonly isComplex: boolean
     move(): LocationValue
     reset(): void
     skipInstance(): void
@@ -36,7 +38,7 @@ export interface LocationIterator extends Iterator<LocationValue> {
 type LocationGetter = (groupIndex: number, instanceIndex: number) => Location
 type IsSecondaryGetter = (groupIndex: number, instanceIndex: number) => boolean
 
-export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
+export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isComplex = false, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
     const value: LocationValue = {
         location: NullLocation as Location,
         index: 0,
@@ -55,6 +57,7 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
         get isNextNewInstance () { return isNextNewInstance },
         get groupCount () { return groupCount },
         get instanceCount () { return instanceCount },
+        isComplex,
         move() {
             if (hasNext) {
                 value.groupIndex = groupIndex
diff --git a/src/mol-geo/util/marker-data.ts b/src/mol-geo/util/marker-data.ts
index 55dc2494560d9bc734dba5a1a11f73f835f797b7..ed813f2fa8432c84960cab9368716ff21eda7f8b 100644
--- a/src/mol-geo/util/marker-data.ts
+++ b/src/mol-geo/util/marker-data.ts
@@ -18,7 +18,6 @@ export enum MarkerAction {
     RemoveHighlight,
     Select,
     Deselect,
-    ToggleSelect,
     Clear
 }
 
@@ -30,42 +29,26 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
             case MarkerAction.Highlight:
                 if (v % 2 === 0) {
                     v += 1
-                    changed = true
                 }
                 break
             case MarkerAction.RemoveHighlight:
                 if (v % 2 !== 0) {
                     v -= 1
-                    changed = true
                 }
                 break
             case MarkerAction.Select:
                 v += 2
-                changed = true
                 break
             case MarkerAction.Deselect:
                 if (v >= 2) {
                     v -= 2
-                    changed = true
                 }
                 break
-            case MarkerAction.ToggleSelect:
-                if (v === 0) {
-                    v = 2
-                } else if (v === 1) {
-                    v = 3
-                } else if (v === 2) {
-                    v = 0
-                } else {
-                    v -= 2
-                }
-                changed = true
-                break
             case MarkerAction.Clear:
                 v = 0
-                changed = true
                 break
         }
+        changed = array[i] !== v || changed
         array[i] = v
     }
     return changed
diff --git a/src/mol-geo/util/size-data.ts b/src/mol-geo/util/size-data.ts
index f1a59a8fa6448b1931576d5d8c3b3b2f4e0af48c..2afbf89eee56c8c0d9bbfe32f3ad56e727d26037 100644
--- a/src/mol-geo/util/size-data.ts
+++ b/src/mol-geo/util/size-data.ts
@@ -9,6 +9,7 @@ import { Vec2 } from 'mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
 import { LocationIterator } from './location-iterator';
 import { Location, NullLocation } from 'mol-model/location';
+import { RuntimeContext } from 'mol-task';
 
 export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance'
 
@@ -48,7 +49,7 @@ export function createValueSize(value: number, sizeData?: SizeData): SizeData {
 }
 
 /** Creates size uniform */
-export function createUniformSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createUniformSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     return createValueSize(sizeFn(NullLocation), sizeData)
 }
 
@@ -72,36 +73,51 @@ export function createTextureSize(sizes: TextureImage, type: SizeType, sizeData?
 }
 
 /** Creates size texture with size for each instance/unit */
-export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { instanceCount} = locationIt
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= instanceCount ? sizeData.tSize.ref.value : createTextureImage(instanceCount, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.instanceIndex] = sizeFn(v.location)
         locationIt.skipInstance()
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating instance sizes', current: i, max: instanceCount });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'instance', sizeData)
 }
 
 /** Creates size texture with size for each group (i.e. shared across instances/units) */
-export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createGroupSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { groupCount } = locationIt
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= groupCount ? sizeData.tSize.ref.value : createTextureImage(groupCount, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.groupIndex] = sizeFn(v.location)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group sizes', current: i, max: groupCount });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'group', sizeData)
 }
 
 /** Creates size texture with size for each group in each instance (i.e. for each unit) */
-export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
+export async function createGroupInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
     const sizes = sizeData && sizeData.tSize.ref.value.array.length >= count ? sizeData.tSize.ref.value : createTextureImage(count, 1)
+    let i = 0
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
         sizes.array[v.index] = sizeFn(v.location)
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Creating group instance sizes', current: i, max: count });
+        }
+        ++i
     }
     return createTextureSize(sizes, 'groupInstance', sizeData)
 }
\ No newline at end of file
diff --git a/src/mol-geo/util/transform-data.ts b/src/mol-geo/util/transform-data.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c1f83a7c5a095022def40d9eca3fc079755d1863
--- /dev/null
+++ b/src/mol-geo/util/transform-data.ts
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util';
+import { Mat4 } from 'mol-math/linear-algebra';
+import { fillSerial } from 'mol-util/array';
+import { Unit } from 'mol-model/structure';
+
+export type TransformData = {
+    aTransform: ValueCell<Float32Array>,
+    uInstanceCount: ValueCell<number>,
+    instanceCount: ValueCell<number>,
+    aInstance: ValueCell<Float32Array>,
+}
+
+export function _createTransforms(transformArray: Float32Array, instanceCount: number, transformData?: TransformData): TransformData {
+    if (transformData) {
+        ValueCell.update(transformData.aTransform, transformArray)
+        ValueCell.update(transformData.uInstanceCount, instanceCount)
+        ValueCell.update(transformData.instanceCount, instanceCount)
+        const aInstance = transformData.aInstance.ref.value.length >= instanceCount ? transformData.aInstance.ref.value : new Float32Array(instanceCount)
+        ValueCell.update(transformData.aInstance, fillSerial(aInstance, instanceCount))
+        return transformData
+    } else {
+        return {
+            aTransform: ValueCell.create(transformArray),
+            uInstanceCount: ValueCell.create(instanceCount),
+            instanceCount: ValueCell.create(instanceCount),
+            aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount)))
+        }
+    }
+}
+
+const identityTransform = new Float32Array(16)
+Mat4.toArray(Mat4.identity(), identityTransform, 0)
+export function createIdentityTransform(transformData?: TransformData): TransformData {
+    return _createTransforms(identityTransform, 1, transformData)
+}
+
+export function createTransforms({ units }: Unit.SymmetryGroup, transformData?: TransformData) {
+    const unitCount = units.length
+    const n = unitCount * 16
+    const array = transformData && transformData.aTransform.ref.value.length >= n ? transformData.aTransform.ref.value : new Float32Array(n)
+    for (let i = 0; i < unitCount; i++) {
+        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
+    }
+    return _createTransforms(array, unitCount, transformData)
+}
+
diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts
index 312ab7d4dc299e1dbb0b74c3f9fe4d246c4e4827..9f64d61e805ef12a33636b639909f572ef2c7c55 100644
--- a/src/mol-gl/renderable/util.ts
+++ b/src/mol-gl/renderable/util.ts
@@ -9,10 +9,10 @@ import { Mat4, Vec3 } from 'mol-math/linear-algebra'
 import { ValueCell } from 'mol-util';
 
 export function calculateTextureInfo (n: number, itemSize: number) {
-    const sqN = Math.sqrt(n * itemSize)
+    const sqN = Math.sqrt(n)
     let width = Math.ceil(sqN)
     width = width + (itemSize - (width % itemSize)) % itemSize
-    const height = width > 0 ? Math.ceil(n * itemSize / width) : 0
+    const height = width > 0 ? Math.ceil(n / width) : 0
     return { width, height, length: width * height * itemSize }
 }
 
@@ -43,7 +43,7 @@ function getPositionDataFromValues(values: PositionValues) {
     }
 }
 
-export function calculateBoundingSphereFromValues(values: PositionValues){
+export function calculateBoundingSphereFromValues(values: PositionValues) {
     const { position, positionCount, transform, transformCount } = getPositionDataFromValues(values)
     return calculateBoundingSphere(position, positionCount, transform, transformCount)
 }
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index 22e64a84405ff4e48940befa7e46dcd04113dd8b..45e6d11f24ba1fcb719467e72ab6cf20b6706bb7 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -92,16 +92,28 @@ namespace Renderer {
                     program.setUniforms(globalUniforms)
                     currentProgramId = program.id
                 }
-                if (r.values.dDoubleSided.ref.value) {
-                    gl.disable(gl.CULL_FACE)
+
+                if (r.values.dDoubleSided) {
+                    if (r.values.dDoubleSided.ref.value) {
+                        gl.disable(gl.CULL_FACE)
+                    } else {
+                        gl.enable(gl.CULL_FACE)
+                    }
                 } else {
-                    gl.enable(gl.CULL_FACE)
+                    // webgl default
+                    gl.disable(gl.CULL_FACE)
                 }
 
-                if (r.values.dFlipSided.ref.value) {
-                    gl.frontFace(gl.CW)
-                    gl.cullFace(gl.FRONT)
+                if (r.values.dFlipSided) {
+                    if (r.values.dFlipSided.ref.value) {
+                        gl.frontFace(gl.CW)
+                        gl.cullFace(gl.FRONT)
+                    } else {
+                        gl.frontFace(gl.CCW)
+                        gl.cullFace(gl.BACK)
+                    }
                 } else {
+                    // webgl default
                     gl.frontFace(gl.CCW)
                     gl.cullFace(gl.BACK)
                 }
diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts
index 57764dd718ca6ca31a704306ea8fd8ad5dbda4a0..71e1adbc6b843f61a825ba020cf3b4420b6cf01f 100644
--- a/src/mol-gl/scene.ts
+++ b/src/mol-gl/scene.ts
@@ -60,10 +60,10 @@ namespace Scene {
 
             update: () => {
                 update()
-                renderableMap.forEach((o, r) => o.update())
+                renderableMap.forEach(o => o.update())
                 boundingSphere = undefined
             },
-            
+
             add: (o: RenderObject) => {
                 if (!renderableMap.has(o)) {
                     renderableMap.set(o, createRenderable(ctx, o))
diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts
index 2fa41cdaea2b58a1bab762bca1a56b19c0da0dcf..93f933e6bb7e8ec21921d1a37775da287f0e0696 100644
--- a/src/mol-gl/shader-code.ts
+++ b/src/mol-gl/shader-code.ts
@@ -6,25 +6,33 @@
  */
 
 import { ValueCell } from 'mol-util';
+import { idFactory } from 'mol-util/id-factory';
 
 export type DefineKind = 'boolean' | 'string'
 export type DefineType = boolean | string
 export type DefineValues = { [k: string]: ValueCell<DefineType> }
 
+const shaderCodeId = idFactory()
+
 export interface ShaderCode {
+    id: number
     vert: string
     frag: string
 }
 
-export const PointShaderCode: ShaderCode = {
-    vert: require('mol-gl/shader/point.vert'),
-    frag: require('mol-gl/shader/point.frag')
+export function ShaderCode(vert: string, frag: string): ShaderCode {
+    return { id: shaderCodeId(), vert, frag }
 }
 
-export const MeshShaderCode: ShaderCode = {
-    vert: require('mol-gl/shader/mesh.vert'),
-    frag: require('mol-gl/shader/mesh.frag')
-}
+export const PointShaderCode = ShaderCode(
+    require('mol-gl/shader/point.vert'),
+    require('mol-gl/shader/point.frag')
+)
+
+export const MeshShaderCode = ShaderCode(
+    require('mol-gl/shader/mesh.vert'),
+    require('mol-gl/shader/mesh.frag')
+)
 
 export type ShaderDefines = {
     [k: string]: ValueCell<DefineType>
@@ -47,9 +55,10 @@ function getDefinesCode (defines: ShaderDefines) {
     return lines.join('\n') + '\n'
 }
 
-export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode) {
+export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode): ShaderCode {
     const header = getDefinesCode(defines)
     return {
+        id: shaderCodeId(),
         vert: `${header}${shaders.vert}`,
         frag: `${header}${shaders.frag}`
     }
diff --git a/src/mol-gl/shader/chunks/assign-color-varying.glsl b/src/mol-gl/shader/chunks/assign-color-varying.glsl
index f4f5dad7358e2e52f052023500745156e891bd75..b946c1025af3ac570541316a7989167747ea8a1f 100644
--- a/src/mol-gl/shader/chunks/assign-color-varying.glsl
+++ b/src/mol-gl/shader/chunks/assign-color-varying.glsl
@@ -5,7 +5,7 @@
 #elif defined(dColorType_group)
     vColor.rgb = readFromTexture(tColor, aGroup, uColorTexDim).rgb;
 #elif defined(dColorType_groupInstance)
-    vColor.rgb = readFromTexture(tColor, aGroup * float(uGroupCount) + aGroup, uColorTexDim).rgb;
+    vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + aGroup, uColorTexDim).rgb;
 #elif defined(dColorType_objectPicking)
     vColor = encodeIdRGBA(float(uObjectId));
 #elif defined(dColorType_instancePicking)
diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl b/src/mol-gl/shader/chunks/assign-material-color.glsl
index 5969c88c34b534a0556d2da9f30e6b22f41a557e..11b1779411269ab223e03cbbc42a90865a286d69 100644
--- a/src/mol-gl/shader/chunks/assign-material-color.glsl
+++ b/src/mol-gl/shader/chunks/assign-material-color.glsl
@@ -1,5 +1,5 @@
 #if defined(dColorType_uniform)
-    vec4 material = vec4(uColor, 1.0);
+    vec4 material = vec4(uColor, uAlpha);
 #elif defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance) || defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
     vec4 material = vColor;
 #endif
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/common-frag-params.glsl b/src/mol-gl/shader/chunks/common-frag-params.glsl
index a1b1b64d8eb9b2a560e4de1b101bb5cc331748ab..c757d1b2f5afa54efad4236d533b9f4535bc64da 100644
--- a/src/mol-gl/shader/chunks/common-frag-params.glsl
+++ b/src/mol-gl/shader/chunks/common-frag-params.glsl
@@ -6,6 +6,10 @@ uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
 varying float vMarker;
 
+varying vec3 vViewPosition;
+
 uniform float uFogNear;
 uniform float uFogFar;
-uniform vec3 uFogColor;
\ No newline at end of file
+uniform vec3 uFogColor;
+
+uniform float uAlpha;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/common-vert-params.glsl b/src/mol-gl/shader/chunks/common-vert-params.glsl
index 30bc2caea18865056975654eeccac9b43348ba66..5028d0abb23450b2c7126a1ed256a6ebfeff95d6 100644
--- a/src/mol-gl/shader/chunks/common-vert-params.glsl
+++ b/src/mol-gl/shader/chunks/common-vert-params.glsl
@@ -7,4 +7,7 @@ uniform int uGroupCount;
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
 varying float vMarker;
+
+varying vec3 vViewPosition;
+
 #pragma glslify: readFromTexture = require(../utils/read-from-texture.glsl)
\ No newline at end of file
diff --git a/src/mol-gl/shader/mesh.frag b/src/mol-gl/shader/mesh.frag
index ce282933ea1281f35e1699e3d0acc373ec3a814f..5b6a6e7218aa4d896bed9a19df9692d36bf9f366 100644
--- a/src/mol-gl/shader/mesh.frag
+++ b/src/mol-gl/shader/mesh.frag
@@ -18,12 +18,10 @@ precision highp int;
 uniform vec3 uLightColor;
 uniform vec3 uLightAmbient;
 uniform mat4 uView;
-uniform float uAlpha;
 
 #ifndef dFlatShaded
     varying vec3 vNormal;
 #endif
-varying vec3 vViewPosition;
 
 #pragma glslify: attenuation = require(./utils/attenuation.glsl)
 #pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl)
@@ -74,7 +72,7 @@ void main() {
         // gl_FragColor.a = 1.0;
         // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0);
         gl_FragColor.rgb = finalColor;
-        gl_FragColor.a = uAlpha;
+        gl_FragColor.a = material.a;
 
         #pragma glslify: import('./chunks/apply-marker-color.glsl')
         #pragma glslify: import('./chunks/apply-fog.glsl')
diff --git a/src/mol-gl/shader/mesh.vert b/src/mol-gl/shader/mesh.vert
index af38be41e65a3be4d05412a1cadd618ce5d38438..b3f1a943085fda68654f05d072db68d6bc803577 100644
--- a/src/mol-gl/shader/mesh.vert
+++ b/src/mol-gl/shader/mesh.vert
@@ -20,8 +20,6 @@ attribute float aGroup;
     varying vec3 vNormal;
 #endif
 
-varying vec3 vViewPosition;
-
 #pragma glslify: inverse = require(./utils/inverse.glsl)
 #pragma glslify: transpose = require(./utils/transpose.glsl)
 
diff --git a/src/mol-gl/shader/point.frag b/src/mol-gl/shader/point.frag
index ae18b4a35f8ac2bf100ba5b52873c1bb5ab4d1e5..861bfef3fa6b59cf498fb9f57747fde9f2a1de94 100644
--- a/src/mol-gl/shader/point.frag
+++ b/src/mol-gl/shader/point.frag
@@ -10,12 +10,10 @@ precision highp int;
 #pragma glslify: import('./chunks/common-frag-params.glsl')
 #pragma glslify: import('./chunks/color-frag-params.glsl')
 
-uniform float uAlpha;
-
 void main(){
     #pragma glslify: import('./chunks/assign-material-color.glsl')
-    
-    gl_FragColor = vec4(material, uAlpha);
+
+    gl_FragColor = material;
 
     #pragma glslify: import('./chunks/apply-marker-color.glsl')
     #pragma glslify: import('./chunks/apply-fog.glsl')
diff --git a/src/mol-gl/shader/point.vert b/src/mol-gl/shader/point.vert
index 950ba3db17bef0d249d56e2ebac4e63a0222d3fb..5340044c2adb4d2545c15e6d67ff506da6f58543 100644
--- a/src/mol-gl/shader/point.vert
+++ b/src/mol-gl/shader/point.vert
@@ -17,6 +17,10 @@ uniform float uViewportHeight;
     uniform float uSize;
 #elif defined(dSizeType_attribute)
     attribute float aSize;
+#elif defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance)
+    varying vec4 vSize;
+    uniform vec2 uSizeTexDim;
+    uniform sampler2D tSize;
 #endif
 
 attribute vec3 aPosition;
diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts
index d64d1ca58868a7552bf222f8cd93e9158ff3a3c0..34fa9501e2ca78895a1789634ccb70b5702dcd46 100644
--- a/src/mol-gl/webgl/program.ts
+++ b/src/mol-gl/webgl/program.ts
@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ShaderCode } from '../shader-code'
+import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code'
 import { Context } from './context';
 import { getUniformUpdaters, getTextureUniformUpdaters, UniformValues } from './uniform';
 import { AttributeBuffers } from './buffer';
@@ -12,6 +12,7 @@ import { TextureId, Textures } from './texture';
 import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache';
 import { idFactory } from 'mol-util/id-factory';
 import { RenderableSchema } from '../renderable/schema';
+import { hashFnv32a, hashString } from 'mol-data/util';
 
 const getNextProgramId = idFactory()
 
@@ -46,19 +47,21 @@ function getAttributeLocations(ctx: Context, program: WebGLProgram, schema: Rend
 }
 
 export interface ProgramProps {
+    defineValues: DefineValues,
     shaderCode: ShaderCode,
     schema: RenderableSchema
 }
 
 export function createProgram(ctx: Context, props: ProgramProps): Program {
     const { gl, shaderCache } = ctx
-    const { shaderCode, schema } = props
+    const { defineValues, shaderCode: _shaderCode, schema } = props
 
     const program = gl.createProgram()
     if (program === null) {
         throw new Error('Could not create WebGL program')
     }
 
+    const shaderCode = addShaderDefines(defineValues, _shaderCode)
     const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert })
     const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag })
 
@@ -113,7 +116,14 @@ export type ProgramCache = ReferenceCache<Program, ProgramProps, Context>
 
 export function createProgramCache(): ProgramCache {
     return createReferenceCache(
-        (props: ProgramProps) => JSON.stringify(props),
+        (props: ProgramProps) => {
+            const array = [ props.shaderCode.id ]
+            Object.keys(props.defineValues).forEach(k => {
+                const v = props.defineValues[k].ref.value
+                array.push(hashString(k), typeof v === 'boolean' ? v ? 1 : 0 : hashString(v))
+            })
+            return hashFnv32a(array).toString()
+        },
         (ctx: Context, props: ProgramProps) => createProgram(ctx, props),
         (program: Program) => { program.destroy() }
     )
diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts
index 284591374c09de8e144518f3bff698b72036fc20..d7277fec66e11fe6630b8165714f5c400f9c6242 100644
--- a/src/mol-gl/webgl/render-item.ts
+++ b/src/mol-gl/webgl/render-item.ts
@@ -7,7 +7,7 @@
 import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, ArrayKind } from './buffer';
 import { createTextures } from './texture';
 import { Context } from './context';
-import { ShaderCode, addShaderDefines } from '../shader-code';
+import { ShaderCode } from '../shader-code';
 import { Program } from './program';
 import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
@@ -74,7 +74,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
     Object.keys(RenderVariantDefines).forEach(k => {
         const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
         programs[k] = programCache.get(ctx, {
-            shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode),
+            defineValues: { ...defineValues, ...variantDefineValues },
+            shaderCode,
             schema
         })
     })
@@ -117,7 +118,7 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
             program.setUniforms(uniformValues)
             if (oesVertexArrayObject && vertexArray) {
                 oesVertexArrayObject.bindVertexArrayOES(vertexArray)
-                // TODO need to bind elements buffer explicitely since it is not always recorded in the VAO
+                // need to bind elements buffer explicitely since it is not always recorded in the VAO
                 if (elementsBuffer) elementsBuffer.bind()
             } else {
                 if (elementsBuffer) elementsBuffer.bind()
@@ -147,7 +148,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S
                     const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k]
                     programs[k].free()
                     programs[k] = programCache.get(ctx, {
-                        shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode),
+                        defineValues: { ...defineValues, ...variantDefineValues },
+                        shaderCode,
                         schema
                     })
                 })
diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts
index 7abb47781969df8deb176c5e1f9918f3cf2b532d..db2409a1ba38052418d98605c79918063bf61db2 100644
--- a/src/mol-math/geometry/lookup3d/grid.ts
+++ b/src/mol-math/geometry/lookup3d/grid.ts
@@ -159,7 +159,7 @@ function _build(state: BuildState): Grid3D {
 function build(data: PositionData) {
     const boundingBox = Box3D.computeBounding(data);
     // need to expand the grid bounds to avoid rounding errors
-    const expandedBox = Box3D.expand(boundingBox, Vec3.create(0.5, 0.5, 0.5));
+    const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
     const boundingSphere = Sphere3D.computeBounding(data);
     const { indices } = data;
 
diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts
index 6d0b5bd5f7da032fed3942b44558ef6d4f279ecc..8b48c524a31614bd354ef8468068071240007e8f 100644
--- a/src/mol-math/geometry/primitives/box3d.ts
+++ b/src/mol-math/geometry/primitives/box3d.ts
@@ -2,9 +2,10 @@
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Vec3 } from '../../linear-algebra'
+import { Vec3, Mat4 } from '../../linear-algebra'
 import { PositionData } from '../common'
 import { OrderedSet } from 'mol-data/int';
 
@@ -12,10 +13,11 @@ interface Box3D { min: Vec3, max: Vec3 }
 
 namespace Box3D {
     export function create(min: Vec3, max: Vec3): Box3D { return { min, max }; }
+    export function empty(): Box3D { return { min: Vec3.zero(), max: Vec3.zero() }; }
 
     export function computeBounding(data: PositionData): Box3D {
-        const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
-        const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];
+        const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
+        const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
 
         const { x, y, z, indices } = data;
         for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
@@ -27,18 +29,49 @@ namespace Box3D {
             max[1] = Math.max(y[i], max[1]);
             max[2] = Math.max(z[i], max[2]);
         }
-        return { min: Vec3.create(min[0], min[1], min[2]), max: Vec3.create(max[0], max[1], max[2]) }
+        return { min, max }
     }
 
-    export function size(box: Box3D) {
-        return Vec3.sub(Vec3.zero(), box.max, box.min);
+    /** Get size of the box */
+    export function size(size: Vec3, box: Box3D): Vec3 {
+        return Vec3.sub(size, box.max, box.min);
     }
 
-    export function expand(box: Box3D, delta: Vec3): Box3D {
-        return {
-            min: Vec3.sub(Vec3.zero(), box.min, delta),
-            max: Vec3.add(Vec3.zero(), box.max, delta)
-        }
+    export function setEmpty(box: Box3D): Box3D {
+        Vec3.set(box.min, Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+        Vec3.set(box.max, -Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+        return box
+    }
+
+    /** Add point to box */
+    export function add(box: Box3D, point: Vec3): Box3D {
+        Vec3.min(box.min, box.min, point)
+        Vec3.max(box.max, box.max, point)
+        return box
+    }
+
+    /** Expand box by delta */
+    export function expand(out: Box3D, box: Box3D, delta: Vec3): Box3D {
+        Vec3.sub(out.min, box.min, delta)
+        Vec3.add(out.max, box.max, delta)
+        return out
+    }
+
+    const tmpTransformV = Vec3.zero()
+    /** Transform box with a Mat4 */
+    export function transform(out: Box3D, box: Box3D, m: Mat4): Box3D {
+        const [ minX, minY, minZ ] = box.min
+        const [ maxX, maxY, maxZ ] = box.max
+        setEmpty(out)
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, maxZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, minZ), m))
+        add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, maxZ), m))
+        return out
     }
 }
 
diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts
index 2132583d80c6937669c3d3186603d804bd8793c3..c38e9275a421d9935b913c028091c5e4370eb11d 100644
--- a/src/mol-math/geometry/primitives/sphere3d.ts
+++ b/src/mol-math/geometry/primitives/sphere3d.ts
@@ -2,18 +2,18 @@
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Vec3 } from '../../linear-algebra'
+import { Vec3, Mat4 } from '../../linear-algebra'
 import { PositionData } from '../common'
 import { OrderedSet } from 'mol-data/int';
 
 interface Sphere3D { center: Vec3, radius: number }
 
 namespace Sphere3D {
-    export function create(center: Vec3, radius: number): Sphere3D {
-        return { center, radius };
-    }
+    export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; }
+    export function zero(): Sphere3D { return { center: Vec3.zero(), radius: 0 }; }
 
     export function computeBounding(data: PositionData): Sphere3D {
         const { x, y, z, indices } = data;
@@ -43,6 +43,13 @@ namespace Sphere3D {
 
         return { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) };
     }
+
+    /** Transform sphere with a Mat4 */
+    export function transform(out: Sphere3D, sphere: Sphere3D, m: Mat4): Sphere3D {
+        Vec3.transformMat4(out.center, sphere.center, m)
+        out.radius = sphere.radius * Mat4.getMaxScaleOnAxis(m)
+        return out
+    }
 }
 
 export { Sphere3D }
\ No newline at end of file
diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts
index aa575c77289110a74d22d197f439c239c0c37ec0..6c233e11d5b49f3d544893a42d8fad372486a181 100644
--- a/src/mol-math/geometry/symmetry-operator.ts
+++ b/src/mol-math/geometry/symmetry-operator.ts
@@ -92,7 +92,7 @@ function isW1(m: Mat4) {
     return m[3] === 0 && m[7] === 0 && m[11] === 0 && m[15] === 1;
 }
 
-function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[0], yy = m[4], zz = m[8], tx = m[12];
 
     if (isW1(m)) {
@@ -106,7 +106,7 @@ function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme
     }
 }
 
-function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[1], yy = m[5], zz = m[9], ty = m[13];
 
     if (isW1(m)) {
@@ -120,7 +120,7 @@ function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme
     }
 }
 
-function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) {
+function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     const xx = m[2], yy = m[6], zz = m[10], tz = m[14];
 
     if (isW1(m)) {
diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts
index ddc230eb5c8cf3c45fa68129d8f2a15be19b1248..442f8e3927a3f2ad3358fabba82a26cbe01096fb 100644
--- a/src/mol-math/linear-algebra/3d/mat4.ts
+++ b/src/mol-math/linear-algebra/3d/mat4.ts
@@ -873,6 +873,13 @@ namespace Mat4 {
         return out;
     }
 
+    export function getMaxScaleOnAxis(m: Mat4) {
+        const scaleXSq = m[0] * m[0] + m[1] * m[1] + m[2] * m[2]
+        const scaleYSq = m[4] * m[4] + m[5] * m[5] + m[6] * m[6]
+        const scaleZSq = m[8] * m[8] + m[9] * m[9] + m[10] * m[10]
+        return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq))
+    }
+
     /** Rotation matrix for 90deg around x-axis */
     export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(1, 0, 0))
     /** Rotation matrix for 180deg around x-axis */
diff --git a/src/mol-model-props/rcsb/symmetry.ts b/src/mol-model-props/rcsb/symmetry.ts
index b3114645c81af51635607625ccdd9b592f86f210..c1727ae3a9fbddd7c45be8fabd32f4e802aa23f6 100644
--- a/src/mol-model-props/rcsb/symmetry.ts
+++ b/src/mol-model-props/rcsb/symmetry.ts
@@ -22,8 +22,8 @@ const { str, int, float, Aliased, Vector, List } = Column.Schema;
 
 function getInstance(name: keyof AssemblySymmetry.Schema): (ctx: CifExportContext) => CifWriter.Category.Instance<any, any> {
     return function(ctx: CifExportContext) {
-        const db = AssemblySymmetry.get(ctx.model);
-        return db ? Category.ofTable(db[name]) : CifWriter.Category.Empty;
+        const assemblySymmetry = AssemblySymmetry.get(ctx.model);
+        return assemblySymmetry ? Category.ofTable(assemblySymmetry.db[name]) : CifWriter.Category.Empty;
     }
 }
 
@@ -38,7 +38,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb
     const clusterRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_cluster>[] = []
     const axisRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_axis>[] = []
 
-    let id = 0
+    let id = 1 // start feature ids at 1
     for (let i = 0, il = assemblies.length; i < il; ++i) {
         const { assembly_id: _assembly_id, rcsb_assembly_symmetry } = assemblies[i]
         if (!rcsb_assembly_symmetry) continue
@@ -65,7 +65,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb
                     clusterRows.push({
                         feature_id: id,
                         avg_rmsd: c.avg_rmsd || 0, // TODO upstream, should not be nullable, or???
-                        members: c.members as string[]
+                        members: c.members as string[] // TODO upstream, array members should not be nullable
                     })
                 }
             }
@@ -109,6 +109,26 @@ const _Descriptor: ModelPropertyDescriptor = {
 
 const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql')
 
+export interface AssemblySymmetry {
+    db: AssemblySymmetry.Database
+    getFeatures(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_feature']>
+    getClusters(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_cluster']>
+    getAxes(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_axis']>
+}
+
+export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetry {
+    const f = db.rcsb_assembly_symmetry_feature
+    const c = db.rcsb_assembly_symmetry_cluster
+    const a = db.rcsb_assembly_symmetry_axis
+
+    return {
+        db,
+        getFeatures: (assemblyId: string) => Table.pick(f, f._schema, i => f.assembly_id.value(i) === assemblyId),
+        getClusters: (featureId: number) => Table.pick(c, c._schema, i => c.feature_id.value(i) === featureId),
+        getAxes: (featureId: number) => Table.pick(a, a._schema, i => a.feature_id.value(i) === featureId)
+    }
+}
+
 export namespace AssemblySymmetry {
     export const Schema = {
         rcsb_assembly_symmetry_feature: {
@@ -195,11 +215,11 @@ export namespace AssemblySymmetry {
         }
 
         model.customProperties.add(Descriptor);
-        model._staticPropertyData.__AssemblySymmetry__ = db;
+        model._staticPropertyData.__AssemblySymmetry__ = AssemblySymmetry(db);
         return true;
     }
 
-    export function get(model: Model): Database | undefined {
+    export function get(model: Model): AssemblySymmetry | undefined {
         return model._staticPropertyData.__AssemblySymmetry__;
     }
 }
\ No newline at end of file
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index d84498e133ae0e709429c81c6b57a5a715f981b2..8e0b1c7157a542549cfd05a46eac617072a00c8a 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -22,4 +22,19 @@ export function isEmptyLoci(x: any): x is EmptyLoci {
     return !!x && x.kind === 'empty-loci';
 }
 
+export function areLociEqual(lociA: Loci, lociB: Loci) {
+    if (isEveryLoci(lociA) && isEveryLoci(lociB)) return true
+    if (isEmptyLoci(lociA) && isEmptyLoci(lociB)) return true
+    if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) {
+        return StructureElement.areLociEqual(lociA, lociB)
+    }
+    if (Link.isLoci(lociA) && Link.isLoci(lociB)) {
+        return Link.areLociEqual(lociA, lociB)
+    }
+    if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) {
+        return Shape.areLociEqual(lociA, lociB)
+    }
+    return false
+}
+
 export type Loci =  StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci
\ No newline at end of file
diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts
index 58f930b6ed8e685b4038c2be3f06ff4a9d417691..ffc602342db05a6419dd471d38b755ff41c51fa3 100644
--- a/src/mol-model/shape/shape.ts
+++ b/src/mol-model/shape/shape.ts
@@ -68,4 +68,15 @@ export namespace Shape {
     export function isLoci(x: any): x is Loci {
         return !!x && x.kind === 'group-loci';
     }
+
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.groups.length !== b.groups.length) return false
+        for (let i = 0, il = a.groups.length; i < il; ++i) {
+            const groupA = a.groups[i]
+            const groupB = b.groups[i]
+            if (groupA.shape.id !== groupB.shape.id) return false
+            if (!OrderedSet.areEqual(groupA.ids, groupB.ids)) return false
+        }
+        return true
+    }
 }
\ No newline at end of file
diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts
index 43e072d461944f2a18902fd29d3af69fd09605db..cb9ddfa8c7ec9ab019517bc8883c6f8d542a985e 100644
--- a/src/mol-model/structure/model/formats/mmcif.ts
+++ b/src/mol-model/structure/model/formats/mmcif.ts
@@ -141,7 +141,12 @@ function getFormatData(format: mmCIF_Format): FormatData {
 function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, formatData: FormatData, previous?: Model): Model {
     const atomic = getAtomicHierarchyAndConformation(format, atom_site, entities, formatData, previous);
     if (previous && atomic.sameAsPrevious) {
-        return { ...previous, atomicConformation: atomic.conformation };
+        return {
+            ...previous,
+            id: UUID.create(),
+            modelNum: atom_site.pdbx_PDB_model_num.value(0),
+            atomicConformation: atomic.conformation
+        };
     }
 
     const coarse = EmptyIHMCoarse;
diff --git a/src/mol-model/structure/model/formats/mmcif/assembly.ts b/src/mol-model/structure/model/formats/mmcif/assembly.ts
index e89690e1723ca31eca3c7295114331e97c9c786a..ddc0e6f478fe75f912992338a7e4032dde1c482f 100644
--- a/src/mol-model/structure/model/formats/mmcif/assembly.ts
+++ b/src/mol-model/structure/model/formats/mmcif/assembly.ts
@@ -110,14 +110,14 @@ function expandOperators1(operatorNames: string[][], list: string[][], i: number
 function getAssemblyOperators(matrices: Matrices, operatorNames: string[][], startIndex: number) {
     const operators: SymmetryOperator[] = [];
 
-    let index = startIndex;
     for (let op of operatorNames) {
         let m = Mat4.identity();
         for (let i = 0; i < op.length; i++) {
             Mat4.mul(m, m, matrices.get(op[i])!);
         }
-        index++;
-        operators[operators.length] = SymmetryOperator.create(`A-${index}`, m);
+        // TODO currently using the original operator name for the symmetry operator to be able
+        // to link it to the original operator but it might be clearer to introduce an extra field???
+        operators[operators.length] = SymmetryOperator.create(`A-${op.join(',')}`, m);
     }
 
     return operators;
diff --git a/src/mol-model/structure/structure/carbohydrates/constants.ts b/src/mol-model/structure/structure/carbohydrates/constants.ts
index cad746542b66e98c5890255dfcfe9bd7ecaab7d9..ed3949f122fb46a6bd1c7de14cce975daf741952 100644
--- a/src/mol-model/structure/structure/carbohydrates/constants.ts
+++ b/src/mol-model/structure/structure/carbohydrates/constants.ts
@@ -174,6 +174,19 @@ const Monosaccharides: SaccharideComponent[] = [
     { abbr: 'Psi', name: 'Psicose', color: SaccharideColors.Pink, type: SaccharideType.Assigned },
 ]
 
+export const MonosaccharidesColorTable: [string, Color][] = [
+    ['Glc-family', SaccharideColors.Blue],
+    ['Man-family', SaccharideColors.Green],
+    ['Gal-family', SaccharideColors.Yellow],
+    ['Gul-family', SaccharideColors.Orange],
+    ['Alt-family', SaccharideColors.Pink],
+    ['All-family', SaccharideColors.Purple],
+    ['Tal-family', SaccharideColors.LightBlue],
+    ['Ido-family', SaccharideColors.Blue],
+    ['Fuc-family', SaccharideColors.Red],
+    ['Generic/Unknown/Secondary', SaccharideColors.Secondary],
+]
+
 const CommonSaccharideNames: { [k: string]: string[] } = {
     // Hexose
     Glc: [
diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts
index 0c83fbcd981cba97b08393086738f339d080aa4b..a8ad08e1ae17fcaded071c74d54ab69eaa3c5443 100644
--- a/src/mol-model/structure/structure/element.ts
+++ b/src/mol-model/structure/structure/element.ts
@@ -63,6 +63,17 @@ namespace StructureElement {
         return !!x && x.kind === 'element-loci';
     }
 
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.elements.length !== b.elements.length) return false
+        for (let i = 0, il = a.elements.length; i < il; ++i) {
+            const elementA = a.elements[i]
+            const elementB = b.elements[i]
+            if (elementA.unit.id !== elementB.unit.id) return false
+            if (!OrderedSet.areEqual(elementA.indices, elementB.indices)) return false
+        }
+        return true
+    }
+
     export function isLocation(x: any): x is StructureElement {
         return !!x && x.kind === 'element-location';
     }
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 6bbf69ff3a5729d38ba2b0ba493e86c8a86b6b17..29a2be47685bcf25bc74255f0335db6c3f306f08 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -8,7 +8,7 @@ import { IntMap, SortedArray, Iterator, Segmentation } from 'mol-data/int'
 import { UniqueArray } from 'mol-data/generic'
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model, ElementIndex } from '../model'
-import { sort, arraySwap, hash1, sortArray } from 'mol-data/util';
+import { sort, arraySwap, hash1, sortArray, hashString } from 'mol-data/util';
 import StructureElement from './element'
 import Unit from './unit'
 import { StructureLookup3D } from './util/lookup3d';
@@ -22,9 +22,12 @@ import { ResidueIndex } from '../model/indexing';
 import { Carbohydrates } from './carbohydrates/data';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { idFactory } from 'mol-util/id-factory';
 
 class Structure {
+    /** Maps unit.id to unit */
     readonly unitMap: IntMap<Unit>;
+    /** Array of all units in the structure, sorted by unit.id */
     readonly units: ReadonlyArray<Unit>;
 
     private _props: {
@@ -66,6 +69,7 @@ class Structure {
         return hash;
     }
 
+    /** Returns a new element location iterator */
     elementLocations(): Iterator<StructureElement> {
         return new Structure.ElementLocationIterator(this);
     }
@@ -197,9 +201,10 @@ namespace Structure {
 
     export class StructureBuilder {
         private units: Unit[] = [];
+        private invariantId = idFactory()
 
         addUnit(kind: Unit.Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
-            const unit = Unit.create(this.units.length, kind, model, operator, elements);
+            const unit = Unit.create(this.units.length, this.invariantId(), kind, model, operator, elements);
             this.units.push(unit);
             return unit;
         }
@@ -225,6 +230,10 @@ namespace Structure {
         return s.hashCode;
     }
 
+    export function conformationHash(s: Structure) {
+        return hashString(s.units.map(u => Unit.conformationId(u)).join('|'))
+    }
+
     export function areEqual(a: Structure, b: Structure) {
         if (a.elementCount !== b.elementCount) return false;
         const len = a.units.length;
diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts
index e3e9c4b6b4b3a8fa45a5a8dc8aef1f7ded41598b..1b35f8341f75d86dccf08e88b08cc8a475e3506f 100644
--- a/src/mol-model/structure/structure/symmetry.ts
+++ b/src/mol-model/structure/structure/symmetry.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import Structure from './structure'
@@ -10,7 +11,7 @@ import { ModelSymmetry } from '../model'
 import { Task, RuntimeContext } from 'mol-task';
 import { SortedArray } from 'mol-data/int';
 import Unit from './unit';
-import { EquivalenceClasses, hash2 } from 'mol-data/util';
+import { EquivalenceClasses } from 'mol-data/util';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry';
 
@@ -58,16 +59,12 @@ namespace StructureSymmetry {
         return Task.create('Build NCS', ctx => _buildNCS(ctx, structure));
     }
 
-    function hashUnit(u: Unit) {
-        return hash2(u.invariantId, SortedArray.hashCode(u.elements));
-    }
-
     export function areUnitsEquivalent(a: Unit, b: Unit) {
         return a.invariantId === b.invariantId && a.model.id === b.model.id && SortedArray.areEqual(a.elements, b.elements);
     }
 
     export function UnitEquivalenceBuilder() {
-        return EquivalenceClasses<number, Unit>(hashUnit, areUnitsEquivalent);
+        return EquivalenceClasses<number, Unit>(Unit.hashUnit, areUnitsEquivalent);
     }
 
     export function computeTransformGroups(s: Structure): ReadonlyArray<Unit.SymmetryGroup> {
@@ -76,12 +73,7 @@ namespace StructureSymmetry {
 
         const ret: Unit.SymmetryGroup[] = [];
         for (const eqUnits of groups.groups) {
-            const first = s.unitMap.get(eqUnits[0]);
-            ret.push({
-                elements: first.elements,
-                units: eqUnits.map(id => s.unitMap.get(id)),
-                hashCode: hashUnit(first)
-            });
+            ret.push(Unit.SymmetryGroup(eqUnits.map(id => s.unitMap.get(id))))
         }
 
         return ret;
diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts
index 3bb205123d1961beddc044f7add9ab6ea7b3bc6c..4092d9d3cbb5521fe300c6c8cf45e35e9f845c0b 100644
--- a/src/mol-model/structure/structure/unit.ts
+++ b/src/mol-model/structure/structure/unit.ts
@@ -2,18 +2,22 @@
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model } from '../model'
 import { GridLookup3D, Lookup3D } from 'mol-math/geometry'
-import { idFactory } from 'mol-util/id-factory';
 import { IntraUnitLinks, computeIntraUnitBonds } from './unit/links'
 import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation } from '../model/properties/coarse';
 import { ValueRef } from 'mol-util';
 import { UnitRings } from './unit/rings';
 import StructureElement from './element'
-import { ChainIndex, ResidueIndex } from '../model/indexing';
+import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing';
+import { IntMap, SortedArray } from 'mol-data/int';
+import { hash2 } from 'mol-data/util';
+import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer';
+import { getNucleotideElements } from './util/nucleotide';
 
 // A building block of a structure that corresponds to an atomic or a coarse grained representation
 // 'conveniently grouped together'.
@@ -27,27 +31,54 @@ namespace Unit {
     export function isSpheres(u: Unit): u is Spheres { return u.kind === Kind.Spheres; }
     export function isGaussians(u: Unit): u is Gaussians { return u.kind === Kind.Gaussians; }
 
-    export function create(id: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
+    export function create(id: number, invariantId: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit {
         switch (kind) {
-            case Kind.Atomic: return new Atomic(id, unitIdFactory(), model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties());
-            case Kind.Spheres: return createCoarse(id, unitIdFactory(), model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)));
-            case Kind.Gaussians: return createCoarse(id, unitIdFactory(), model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)));
+            case Kind.Atomic: return new Atomic(id, invariantId, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties());
+            case Kind.Spheres: return createCoarse(id, invariantId, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)), CoarseProperties());
+            case Kind.Gaussians: return createCoarse(id, invariantId, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)), CoarseProperties());
         }
     }
 
     /** A group of units that differ only by symmetry operators. */
     export type SymmetryGroup = {
-        readonly elements: StructureElement.Set,
+        readonly elements: StructureElement.Set
         readonly units: ReadonlyArray<Unit>
+        /** Maps unit.id to index of unit in units array */
+        readonly unitIndexMap: IntMap<number>
         readonly hashCode: number
     }
 
-    /** Find index of unit with given id, returns -1 if not found */
-    export function findUnitById(id: number, units: ReadonlyArray<Unit>) {
-        for (let i = 0, il = units.length; i < il; ++i) {
-            if (units[i].id === id) return i
+    function getUnitIndexMap(units: Unit[]) {
+        const unitIndexMap = IntMap.Mutable<number>();
+        for (let i = 0, _i = units.length; i < _i; i++) {
+            unitIndexMap.set(units[i].id, i);
+        }
+        return unitIndexMap
+    }
+
+    export function SymmetryGroup(units: Unit[]) {
+        const props: {
+            unitIndexMap?: IntMap<number>
+        } = {}
+
+        return {
+            elements: units[0].elements,
+            units,
+            get unitIndexMap () {
+                if (props.unitIndexMap) return props.unitIndexMap
+                props.unitIndexMap = getUnitIndexMap(units)
+                return props.unitIndexMap
+            },
+            hashCode: hashUnit(units[0])
         }
-        return -1
+    }
+
+    export function conformationId (unit: Unit) {
+        return Unit.isAtomic(unit) ? unit.model.atomicConformation.id : unit.model.coarseConformation.id
+    }
+
+    export function hashUnit(u: Unit) {
+        return hash2(u.invariantId, SortedArray.hashCode(u.elements));
     }
 
     export interface Base {
@@ -62,6 +93,8 @@ namespace Unit {
         applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
 
         readonly lookup3d: Lookup3D
+        readonly polymerElements: SortedArray<ElementIndex>
+        readonly gapElements: SortedArray<ElementIndex>
     }
 
     function getSphereRadiusFunc(model: Model) {
@@ -74,8 +107,6 @@ namespace Unit {
         return (i: number) => 0;
     }
 
-    const unitIdFactory = idFactory();
-
     // A bulding block of a structure that corresponds
     // to a "natural group of atoms" (most often a "chain")
     // together with a tranformation (rotation and translation)
@@ -127,6 +158,24 @@ namespace Unit {
             return this.props.rings.ref;
         }
 
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getAtomicPolymerElements(this);
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getAtomicGapElements(this);
+            return this.props.gapElements.ref;
+        }
+
+        get nucleotideElements() {
+            if (this.props.nucleotideElements.ref) return this.props.nucleotideElements.ref;
+            this.props.nucleotideElements.ref = getNucleotideElements(this);
+            return this.props.nucleotideElements.ref;
+        }
+
         getResidueIndex(elementIndex: StructureElement.UnitIndex) {
             return this.model.atomicHierarchy.residueAtomSegments.index[this.elements[elementIndex]];
         }
@@ -148,10 +197,20 @@ namespace Unit {
         lookup3d: ValueRef<Lookup3D | undefined>,
         links: ValueRef<IntraUnitLinks | undefined>,
         rings: ValueRef<UnitRings | undefined>
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        nucleotideElements: ValueRef<SortedArray<ElementIndex> | undefined>
     }
 
     function AtomicProperties(): AtomicProperties {
-        return { lookup3d: ValueRef.create(void 0), links: ValueRef.create(void 0), rings: ValueRef.create(void 0) };
+        return {
+            lookup3d: ValueRef.create(void 0),
+            links: ValueRef.create(void 0),
+            rings: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+            nucleotideElements: ValueRef.create(void 0),
+        };
     }
 
     class Coarse<K extends Kind.Gaussians | Kind.Spheres, C extends CoarseSphereConformation | CoarseGaussianConformation> implements Base {
@@ -166,32 +225,45 @@ namespace Unit {
         readonly coarseElements: CoarseElements;
         readonly coarseConformation: C;
 
+        private props: CoarseProperties;
+
         getChild(elements: StructureElement.Set): Unit {
             if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */;
-            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation);
+            return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation, CoarseProperties());
         }
 
         applyOperator(id: number, operator: SymmetryOperator, dontCompose = false): Unit {
             const op = dontCompose ? operator : SymmetryOperator.compose(this.conformation.operator, operator);
-            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r));
-            (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
+            const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r), this.props);
+            // (ret as Coarse<K, C>)._lookup3d = this._lookup3d;
             return ret;
         }
 
-        private _lookup3d: ValueRef<Lookup3D | undefined> = ValueRef.create(void 0);
         get lookup3d() {
-            if (this._lookup3d.ref) return this._lookup3d.ref;
+            if (this.props.lookup3d.ref) return this.props.lookup3d.ref;
             // TODO: support sphere radius?
             const { x, y, z } = this.getCoarseElements();
-            this._lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
-            return this._lookup3d.ref;
+            this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
+            return this.props.lookup3d.ref;
+        }
+
+        get polymerElements() {
+            if (this.props.polymerElements.ref) return this.props.polymerElements.ref;
+            this.props.polymerElements.ref = getCoarsePolymerElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.polymerElements.ref;
+        }
+
+        get gapElements() {
+            if (this.props.gapElements.ref) return this.props.gapElements.ref;
+            this.props.gapElements.ref = getCoarseGapElements(this as Unit.Spheres | Unit.Gaussians); // TODO
+            return this.props.gapElements.ref;
         }
 
         private getCoarseElements() {
             return this.kind === Kind.Spheres ? this.model.coarseConformation.spheres : this.model.coarseConformation.gaussians;
         }
 
-        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping) {
+        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) {
             this.kind = kind;
             this.id = id;
             this.invariantId = invariantId;
@@ -200,11 +272,26 @@ namespace Unit {
             this.conformation = conformation;
             this.coarseElements = kind === Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
             this.coarseConformation = (kind === Kind.Spheres ? model.coarseConformation.spheres : model.coarseConformation.gaussians) as C;
+            this.props = props;
         }
     }
 
-    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping): Unit {
-        return new Coarse(id, invariantId, model, kind, elements, conformation) as any as Unit /** lets call this an ugly temporary hack */;
+    interface CoarseProperties {
+        lookup3d: ValueRef<Lookup3D | undefined>,
+        polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
+        gapElements: ValueRef<SortedArray<ElementIndex> | undefined>
+    }
+
+    function CoarseProperties(): CoarseProperties {
+        return {
+            lookup3d: ValueRef.create(void 0),
+            polymerElements: ValueRef.create(void 0),
+            gapElements: ValueRef.create(void 0),
+        };
+    }
+
+    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties): Unit {
+        return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */;
     }
 
     export class Spheres extends Coarse<Kind.Spheres, CoarseSphereConformation> { }
diff --git a/src/mol-model/structure/structure/unit/links.ts b/src/mol-model/structure/structure/unit/links.ts
index 5534b62f0670e027baad12e20c4f5a8187f47192..12f24b5c0ff5908752907602b2177b5913f49aae 100644
--- a/src/mol-model/structure/structure/unit/links.ts
+++ b/src/mol-model/structure/structure/unit/links.ts
@@ -32,6 +32,13 @@ namespace Link {
         return !!x && x.kind === 'link-location';
     }
 
+    export function areLocationsEqual(locA: Location, locB: Location) {
+        return (
+            locA.aIndex === locB.aIndex && locA.bIndex === locB.bIndex &&
+            locA.aUnit.id === locB.aUnit.id && locA.bUnit.id === locB.bUnit.id
+        )
+    }
+
     export interface Loci {
         readonly kind: 'link-loci',
         readonly links: ReadonlyArray<Location>
@@ -45,6 +52,14 @@ namespace Link {
         return !!x && x.kind === 'link-loci';
     }
 
+    export function areLociEqual(a: Loci, b: Loci) {
+        if (a.links.length !== b.links.length) return false
+        for (let i = 0, il = a.links.length; i < il; ++i) {
+            if (!areLocationsEqual(a.links[i], b.links[i])) return false
+        }
+        return true
+    }
+
     export function getType(structure: Structure, link: Location<Unit.Atomic>): LinkType {
         if (link.aUnit === link.bUnit) {
             const links = link.aUnit.links;
diff --git a/src/mol-model/structure/structure/unit/links/data.ts b/src/mol-model/structure/structure/unit/links/data.ts
index 8b570768b55d0fc0ff99e4ff0a0c3dd5a9b66e6e..1ee6243c4b6b2ecda3473a0240a36d63e43a5ef1 100644
--- a/src/mol-model/structure/structure/unit/links/data.ts
+++ b/src/mol-model/structure/structure/unit/links/data.ts
@@ -18,10 +18,13 @@ namespace IntraUnitLinks {
 }
 
 class InterUnitBonds {
+    /** Number of inter-unit bonds */
     readonly bondCount: number
+    /** Array of inter-unit bonds */
     readonly bonds: ReadonlyArray<InterUnitBonds.Bond>
     private readonly bondKeyIndex: Map<string, number>
 
+    /** Get an array of unit-pair-bonds that are linked to the given unit */
     getLinkedUnits(unit: Unit): ReadonlyArray<InterUnitBonds.UnitPairBonds> {
         if (!this.map.has(unit.id)) return emptyArray;
         return this.map.get(unit.id)!;
@@ -34,11 +37,13 @@ class InterUnitBonds {
         return index !== undefined ? index : -1
     }
 
+    /** Get inter-unit bond given a pair of indices and units */
     getBond(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): InterUnitBonds.Bond | undefined {
         const index = this.getBondIndex(indexA, unitA, indexB, unitB)
         return index !== -1 ? this.bonds[index] : undefined
     }
 
+    /** Get inter-unit bond given a link-location */
     getBondFromLocation(l: Link.Location) {
         return this.getBond(l.aIndex, l.aUnit, l.bIndex, l.bUnit);
     }
diff --git a/src/mol-model/structure/structure/util/boundary.ts b/src/mol-model/structure/structure/util/boundary.ts
index 2a595f8cd7551a060e19d12388e7ad222f55dfd7..b90370614883b4518fb361814074a6696bb74090 100644
--- a/src/mol-model/structure/structure/util/boundary.ts
+++ b/src/mol-model/structure/structure/util/boundary.ts
@@ -2,66 +2,105 @@
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import Structure from '../structure'
-import { Box3D, Sphere3D } from 'mol-math/geometry';
+import Unit from '../unit';
+import { Box3D, Sphere3D, SymmetryOperator } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { SortedArray } from 'mol-data/int';
+import { ElementIndex } from '../../model/indexing';
 
-function computeStructureBoundary(s: Structure): { box: Box3D, sphere: Sphere3D } {
-    const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE];
-    const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE];
+export type Boundary = { box: Box3D, sphere: Sphere3D }
 
-    const { units } = s;
+function computeElementsPositionBoundary(elements: SortedArray<ElementIndex>, position: SymmetryOperator.CoordinateMapper): Boundary {
+    const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+    const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+    const center = Vec3.zero()
 
-    let cx = 0, cy = 0, cz = 0;
-    let radiusSq = 0;
-    let size = 0;
+    let radiusSq = 0
+    let size = 0
 
-    for (let i = 0, _i = units.length; i < _i; i++) {
-        const { x, y, z } = units[i].conformation;
-
-        const elements = units[i].elements;
-        size += elements.length;
-        for (let j = 0, _j = elements.length; j < _j; j++) {
-            const e = elements[j];
-            const xx = x(e), yy = y(e), zz = z(e);
-
-            min[0] = Math.min(xx, min[0]);
-            min[1] = Math.min(yy, min[1]);
-            min[2] = Math.min(zz, min[2]);
-            max[0] = Math.max(xx, max[0]);
-            max[1] = Math.max(yy, max[1]);
-            max[2] = Math.max(zz, max[2]);
-
-            cx += xx;
-            cy += yy;
-            cz += zz;
-        }
-    }
+    const p = Vec3.zero()
 
-    if (size > 0) {
-        cx /= size;
-        cy /= size;
-        cz /= size;
+    size += elements.length
+    for (let j = 0, _j = elements.length; j < _j; j++) {
+        position(elements[j], p)
+        Vec3.min(min, min, p)
+        Vec3.max(max, max, p)
+        Vec3.add(center, center, p)
     }
 
-    for (let i = 0, _i = units.length; i < _i; i++) {
-        const { x, y, z } = units[i].conformation;
-
-        const elements = units[i].elements;
-        for (let j = 0, _j = elements.length; j < _j; j++) {
-            const e = elements[j];
-            const dx = x(e) - cx, dy = y(e) - cy, dz = z(e) - cz;
-            const d = dx * dx + dy * dy + dz * dz;
-            if (d > radiusSq) radiusSq = d;
-        }
+    if (size > 0) Vec3.scale(center, center, 1/size)
+
+    for (let j = 0, _j = elements.length; j < _j; j++) {
+        position(elements[j], p)
+        const d = Vec3.squaredDistance(p, center)
+        if (d > radiusSq) radiusSq = d
     }
 
     return {
-        box: { min: Vec3.ofArray(min), max: Vec3.ofArray(max) },
-        sphere: { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) }
-    };
+        box: { min, max },
+        sphere: { center, radius: Math.sqrt(radiusSq) }
+    }
 }
 
-export { computeStructureBoundary }
\ No newline at end of file
+function computeInvariantUnitBoundary(u: Unit): Boundary {
+    return computeElementsPositionBoundary(u.elements, u.conformation.invariantPosition)
+}
+
+export function computeUnitBoundary(u: Unit): Boundary {
+    return computeElementsPositionBoundary(u.elements, u.conformation.position)
+}
+
+const tmpBox = Box3D.empty()
+const tmpSphere = Sphere3D.zero()
+
+export function computeStructureBoundary(s: Structure): Boundary {
+    const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
+    const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE)
+    const center = Vec3.zero()
+
+    const { units } = s
+
+    const boundaryMap: Map<number, Boundary> = new Map()
+    function getInvariantBoundary(u: Unit) {
+        let boundary: Boundary
+        if (boundaryMap.has(u.invariantId)) {
+            boundary = boundaryMap.get(u.invariantId)!
+        } else {
+            boundary = computeInvariantUnitBoundary(u)
+            boundaryMap.set(u.invariantId, boundary)
+        }
+        return boundary
+    }
+
+    let radius = 0
+    let size = 0
+
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        const u = units[i]
+        const invariantBoundary = getInvariantBoundary(u)
+        const m = u.conformation.operator.matrix
+        size += u.elements.length
+        Box3D.transform(tmpBox, invariantBoundary.box, m)
+        Vec3.min(min, min, tmpBox.min)
+        Vec3.max(max, max, tmpBox.max)
+        Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m)
+        Vec3.scaleAndAdd(center, center, tmpSphere.center, u.elements.length)
+    }
+
+    if (size > 0) Vec3.scale(center, center, 1/size)
+
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        const u = units[i]
+        const invariantBoundary = getInvariantBoundary(u)
+        const m = u.conformation.operator.matrix
+        Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m)
+        const d = Vec3.distance(tmpSphere.center, center) + tmpSphere.radius
+        if (d > radius) radius = d
+    }
+
+    return { box: { min, max }, sphere: { center, radius } }
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/util/nucleotide.ts b/src/mol-model/structure/structure/util/nucleotide.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8dbb668b74a5330781041f33c30c7d3d23713351
--- /dev/null
+++ b/src/mol-model/structure/structure/util/nucleotide.ts
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, SortedArray } from 'mol-data/int';
+import { isNucleic, MoleculeType } from 'mol-model/structure/model/types';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+export function getNucleotideElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { chemicalComponentMap } = model.properties
+    const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
+    const { label_comp_id } = residues
+    const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+    while (chainIt.hasNext) {
+        residueIt.setSegment(chainIt.move());
+
+        while (residueIt.hasNext) {
+            const { index } = residueIt.move();
+            const cc = chemicalComponentMap.get(label_comp_id.value(index))
+            const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown
+
+            if (isNucleic(moleculeType)) {
+                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
+            }
+        }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/structure/util/polymer.ts b/src/mol-model/structure/structure/util/polymer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3e9825b4e8f674e1be14efddbeeed2ec77c01e3
--- /dev/null
+++ b/src/mol-model/structure/structure/util/polymer.ts
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, OrderedSet, Interval, SortedArray } from 'mol-data/int';
+import SortedRanges from 'mol-data/int/sorted-ranges';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+export function getAtomicPolymerElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const polymerIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.polymerRanges, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+    while (polymerIt.hasNext) {
+        const polymerSegment = polymerIt.move()
+        residueIt.setSegment(polymerSegment)
+        while (residueIt.hasNext) {
+            const residueSegment = residueIt.move()
+            const { start, end, index } = residueSegment
+            if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) {
+                const elementIndex = getElementIndexForAtomRole(model, index, 'trace')
+                indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex)
+            }
+        }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarsePolymerElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const polymerRanges = Unit.isSpheres(unit) ? spheres.polymerRanges : gaussians.polymerRanges
+    const polymerIt = SortedRanges.transientSegments(polymerRanges, elements)
+    while (polymerIt.hasNext) {
+        const { start, end } = polymerIt.move()
+        for (let i = start; i < end; ++i) { indices.push(elements[i]) }
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getAtomicGapElements(unit: Unit.Atomic) {
+    const indices: ElementIndex[] = []
+    const { elements, model, residueIndex } = unit
+    const { residueAtomSegments } = unit.model.atomicHierarchy
+    const gapIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.gapRanges, unit.elements);
+    while (gapIt.hasNext) {
+        const gapSegment = gapIt.move();
+        const indexStart = residueIndex[elements[gapSegment.start]]
+        const indexEnd = residueIndex[elements[gapSegment.end - 1]]
+        const elementIndexStart = getElementIndexForAtomRole(model, indexStart, 'trace')
+        const elementIndexEnd = getElementIndexForAtomRole(model, indexEnd, 'trace')
+        indices.push(elementIndexStart === -1 ? residueAtomSegments.offsets[indexStart] : elementIndexStart)
+        indices.push(elementIndexEnd === -1 ? residueAtomSegments.offsets[indexEnd] : elementIndexEnd)
+
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
+
+export function getCoarseGapElements(unit: Unit.Spheres | Unit.Gaussians) {
+    const indices: ElementIndex[] = []
+    const { elements, model } = unit
+    const { spheres, gaussians } = model.coarseHierarchy
+    const gapRanges = Unit.isSpheres(unit) ? spheres.gapRanges : gaussians.gapRanges
+    const gapIt = SortedRanges.transientSegments(gapRanges, elements)
+    while (gapIt.hasNext) {
+        const { start, end } = gapIt.move()
+        indices.push(elements[start], elements[end - 1])
+    }
+    return SortedArray.ofSortedArray<ElementIndex>(indices)
+}
\ No newline at end of file
diff --git a/src/mol-model/structure/util.ts b/src/mol-model/structure/util.ts
index a20abb42e47bf97df10a7d7feb76d9dc01794407..ba416db932b2ef93f2ae7a66884dcbc214773c16 100644
--- a/src/mol-model/structure/util.ts
+++ b/src/mol-model/structure/util.ts
@@ -26,13 +26,13 @@ export function getAtomIdForAtomRole(moleculeType: MoleculeType, atomRole: AtomR
     return ''
 }
 
-export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex {
+export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex | -1 {
     const { offsets } = model.atomicHierarchy.residueAtomSegments
     const { label_atom_id } = model.atomicHierarchy.atoms
     for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) {
         if (label_atom_id.value(j) === atomId) return j as ElementIndex
     }
-    return offsets[rI] as ElementIndex
+    return -1
 }
 
 export function getElementIndexForAtomRole(model: Model, rI: ResidueIndex, atomRole: AtomRole) {
diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts
index e0313d893b7e373774868005566cfbde0f02c1eb..592a2d936f29d5bcf93579af38b70b79acec0737 100644
--- a/src/mol-model/volume/data.ts
+++ b/src/mol-model/volume/data.ts
@@ -24,7 +24,7 @@ namespace VolumeData {
     const _scale = Mat4.zero(), _translate = Mat4.zero();
     export function getGridToCartesianTransform(volume: VolumeData) {
         const { data: { space } } = volume;
-        const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(volume.fractionalBox), Vec3.ofArray(space.dimensions)));
+        const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(Vec3.zero(), volume.fractionalBox), Vec3.ofArray(space.dimensions)));
         const translate = Mat4.fromTranslation(_translate, volume.fractionalBox.min);
         return Mat4.mul3(Mat4.zero(), volume.cell.fromFractional, translate, scale);
     }
diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts
index 11998ff2d6d9391ca612620e34f2e5d5706bb641..6895f81ed2b5b7a5db5172170a0b76656bad1aba 100644
--- a/src/mol-util/array.ts
+++ b/src/mol-util/array.ts
@@ -50,9 +50,8 @@ export function arrayRms(array: Helpers.NumberArray) {
     return Math.sqrt(sumSq / n)
 }
 
-/** Fill an array with serial numbers starting from 0 */
-export function fillSerial<T extends Helpers.NumberArray> (array: T) {
-    const n = array.length
-    for (let i = 0; i < n; ++i) array[ i ] = i
+/** Fill an array with serial numbers starting from 0 until n - 1 (defaults to array.length) */
+export function fillSerial<T extends Helpers.NumberArray> (array: T, n?: number) {
+    for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i
     return array
 }
\ No newline at end of file
diff --git a/src/mol-util/color/color.ts b/src/mol-util/color/color.ts
index ea0c4efc0d8803f383cc94598c1e6bbdff017b41..764875f9470a22faa0e0de4d2d4a8701c41301b1 100644
--- a/src/mol-util/color/color.ts
+++ b/src/mol-util/color/color.ts
@@ -10,6 +10,10 @@ export type Color = { readonly '@type': 'color' } & number
 export function Color(hex: number) { return hex as Color }
 
 export namespace Color {
+    export function toStyle(hexColor: Color) {
+        return `rgb(${hexColor >> 16 & 255}, ${hexColor >> 8 & 255}, ${hexColor & 255})`
+    }
+
     export function toRgb(hexColor: Color) {
         return [ hexColor >> 16 & 255, hexColor >> 8 & 255, hexColor & 255 ]
     }
diff --git a/src/mol-util/color/scale.ts b/src/mol-util/color/scale.ts
index f03b7539969d8af3a7f29f46592d95a9625e8670..ed486d44d46aa382cf8e5f24402b2060e43f6983 100644
--- a/src/mol-util/color/scale.ts
+++ b/src/mol-util/color/scale.ts
@@ -6,6 +6,7 @@
 
 import { Color } from './color'
 import { ColorBrewer } from './tables'
+import { ScaleLegend } from 'mol-view/theme/color';
 
 export interface ColorScale {
     /** Returns hex color for given value */
@@ -14,19 +15,22 @@ export interface ColorScale {
     colorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void
     /** Copies normalized (0 to 1) hex color to rgb array */
     normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void
+    /** */
+    readonly legend: ScaleLegend
 }
 
 export const DefaultColorScale = {
     domain: [0, 1],
     reverse: false,
-    colors: ColorBrewer.RdYlBu as Color[]
+    colors: ColorBrewer.RdYlBu,
 }
 export type ColorScaleProps = Partial<typeof DefaultColorScale>
 
 export namespace ColorScale {
     export function create(props: ColorScaleProps): ColorScale {
-        const { domain, reverse, colors } = { ...DefaultColorScale, ...props }
-        const [ min, max ] = reverse ? domain.slice().reverse() : domain
+        const { domain, reverse, colors: _colors } = { ...DefaultColorScale, ...props }
+        const [ min, max ] = domain
+        const colors = reverse ? _colors.slice().reverse() : _colors
         const count1 = colors.length - 1
         const diff = (max - min) || 1
 
@@ -45,6 +49,7 @@ export namespace ColorScale {
             normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => {
                 Color.toArrayNormalized(color(value), array, offset)
             },
+            get legend() { return ScaleLegend(min, max, colors) }
         }
     }
 }
diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts
index f622969cf430a93fc288f0f899c0278cbfdb83fa..af4e96d4cb0e2257a7f89f8261f932cdeba54740 100644
--- a/src/mol-util/input/input-observer.ts
+++ b/src/mol-util/input/input-observer.ts
@@ -231,10 +231,6 @@ namespace InputObserver {
             window.removeEventListener('resize', onResize, false)
         }
 
-        function preventDefault (ev: Event | Touch) {
-            if ('preventDefault' in ev) ev.preventDefault()
-        }
-
         function onContextMenu(event: Event) {
             if (noContextMenu) {
                 event.preventDefault()
@@ -273,8 +269,6 @@ namespace InputObserver {
         }
 
         function onTouchStart (ev: TouchEvent) {
-            preventDefault(ev)
-
             if (ev.touches.length === 1) {
                 buttons = ButtonsFlag.Primary
                 onPointerDown(ev.touches[0])
@@ -286,13 +280,9 @@ namespace InputObserver {
             }
         }
 
-        function onTouchEnd (ev: TouchEvent) {
-            preventDefault(ev)
-        }
+        function onTouchEnd (ev: TouchEvent) {}
 
         function onTouchMove (ev: TouchEvent) {
-            preventDefault(ev)
-
             if (ev.touches.length === 1) {
                 buttons = ButtonsFlag.Primary
                 onPointerMove(ev.touches[0])
@@ -313,22 +303,16 @@ namespace InputObserver {
         }
 
         function onMouseDown (ev: MouseEvent) {
-            preventDefault(ev)
-
             buttons = getButtons(ev)
             onPointerDown(ev)
         }
 
         function onMouseMove (ev: MouseEvent) {
-            preventDefault(ev)
-
             buttons = getButtons(ev)
             onPointerMove(ev)
         }
 
         function onMouseUp (ev: MouseEvent) {
-            preventDefault(ev)
-
             buttons = getButtons(ev)
             onPointerUp(ev)
         }
diff --git a/src/mol-util/url-query.ts b/src/mol-util/url-query.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a4a10a8371c24b55995ea175df35fb14bc6cec86
--- /dev/null
+++ b/src/mol-util/url-query.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export function urlQueryParameter (id: string) {
+    if (typeof window === 'undefined') return undefined
+    const a = new RegExp(`${id}=([^&#=]*)`)
+    const m = a.exec(window.location.search)
+    return m ? decodeURIComponent(m[1]) : undefined
+}
\ No newline at end of file
diff --git a/src/mol-view/label.ts b/src/mol-view/label.ts
index fe766334660d7072333af05996eebe43e4685d4b..39fad6853ccecbd87d3a706083bc9708eba2d550 100644
--- a/src/mol-view/label.ts
+++ b/src/mol-view/label.ts
@@ -5,12 +5,13 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Unit, StructureElement, StructureProperties as Props } from 'mol-model/structure';
+import { Unit, StructureElement, StructureProperties as Props, Link } from 'mol-model/structure';
 import { Loci } from 'mol-model/loci';
 import { OrderedSet } from 'mol-data/int';
 
-const elementLocA = StructureElement.create()
-const elementLocB = StructureElement.create()
+// for `labelFirst`, don't create right away to avaiod problems with circular dependencies/imports
+let elementLocA: StructureElement
+let elementLocB: StructureElement
 
 function setElementLocation(loc: StructureElement, unit: Unit, index: StructureElement.UnitIndex) {
     loc.unit = unit
@@ -28,14 +29,8 @@ export function labelFirst(loci: Loci): string {
                 return 'Unknown'
             }
         case 'link-loci':
-            const bond = loci.links[0]
-            if (bond) {
-                setElementLocation(elementLocA, bond.aUnit, bond.aIndex)
-                setElementLocation(elementLocB, bond.bUnit, bond.bIndex)
-                return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}`
-            } else {
-                return 'Unknown'
-            }
+            const link = loci.links[0]
+            return link ? linkLabel(link) : 'Unknown'
         case 'group-loci':
             const g = loci.groups[0]
             if (g) {
@@ -50,32 +45,40 @@ export function labelFirst(loci: Loci): string {
     }
 }
 
-export function elementLabel(loc: StructureElement) {
-    const model = loc.unit.model.label
-    const instance = loc.unit.conformation.operator.name
-    let element = ''
+export function linkLabel(link: Link.Location) {
+    if (!elementLocA) elementLocA = StructureElement.create()
+    if (!elementLocB) elementLocB = StructureElement.create()
+    setElementLocation(elementLocA, link.aUnit, link.aIndex)
+    setElementLocation(elementLocB, link.bUnit, link.bIndex)
+    return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}`
+}
+
+export function elementLabel(element: StructureElement) {
+    const model = element.unit.model.label
+    const instance = element.unit.conformation.operator.name
+    let label = ''
 
-    if (Unit.isAtomic(loc.unit)) {
-        const asym_id = Props.chain.auth_asym_id(loc)
-        const seq_id = Props.residue.auth_seq_id(loc)
-        const comp_id = Props.residue.auth_comp_id(loc)
-        const atom_id = Props.atom.auth_atom_id(loc)
-        element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}`
-    } else if (Unit.isCoarse(loc.unit)) {
-        const asym_id = Props.coarse.asym_id(loc)
-        const seq_id_begin = Props.coarse.seq_id_begin(loc)
-        const seq_id_end = Props.coarse.seq_id_end(loc)
+    if (Unit.isAtomic(element.unit)) {
+        const asym_id = Props.chain.auth_asym_id(element)
+        const seq_id = Props.residue.auth_seq_id(element)
+        const comp_id = Props.residue.auth_comp_id(element)
+        const atom_id = Props.atom.auth_atom_id(element)
+        label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}`
+    } else if (Unit.isCoarse(element.unit)) {
+        const asym_id = Props.coarse.asym_id(element)
+        const seq_id_begin = Props.coarse.seq_id_begin(element)
+        const seq_id_end = Props.coarse.seq_id_end(element)
         if (seq_id_begin === seq_id_end) {
-            const entityKey = Props.coarse.entityKey(loc)
-            const seq = loc.unit.model.sequence.byEntityKey[entityKey]
+            const entityKey = Props.coarse.entityKey(element)
+            const seq = element.unit.model.sequence.byEntityKey[entityKey]
             const comp_id = seq.compId.value(seq_id_begin - 1) // 1-indexed
-            element = `[${comp_id}]${seq_id_begin}:${asym_id}`
+            label = `[${comp_id}]${seq_id_begin}:${asym_id}`
         } else {
-            element = `${seq_id_begin}-${seq_id_end}:${asym_id}`
+            label = `${seq_id_begin}-${seq_id_end}:${asym_id}`
         }
     } else {
-        element = 'unknown'
+        label = 'unknown'
     }
 
-    return `${model} ${instance} ${element}`
+    return `${model} ${instance} ${label}`
 }
\ No newline at end of file
diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts
index 6ecac7aa58dcde699b76c542eb2d80852050fb27..271b2f9d703695a6b7717fe17d632e4de7445fd0 100644
--- a/src/mol-view/stage.ts
+++ b/src/mol-view/stage.ts
@@ -122,8 +122,8 @@ export class Stage {
         // this.loadMmcifUrl(`../../examples/1crn.cif`)
         // this.loadPdbid('5u0q') // mixed dna/rna in same polymer
         // this.loadPdbid('1xj9') // PNA (peptide nucleic acid)
-        this.loadPdbid('5eme') // PNA (peptide nucleic acid) and RNA
-        // this.loadPdbid('5eme') // temp
+        // this.loadPdbid('5eme') // PNA (peptide nucleic acid) and RNA
+        this.loadPdbid('2X3T') // temp
 
         // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok
         // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000002.cif`) // ok
diff --git a/src/mol-view/state/transform.ts b/src/mol-view/state/transform.ts
index bda97a320ad07592ce4b0590f791a5b96643bc63..bb454c3c5d7711e2869a085e253fb52c769f2074 100644
--- a/src/mol-view/state/transform.ts
+++ b/src/mol-view/state/transform.ts
@@ -99,7 +99,7 @@ export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEnti
 export const StructureToSpacefill: StructureToSpacefill = StateTransform.create('structure', 'spacefill', 'structure-to-spacefill',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<SpacefillProps> = {}) {
         const spacefillRepr = SpacefillRepresentation()
-        await spacefillRepr.create(structureEntity.value, props).run(ctx.log)
+        await spacefillRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(spacefillRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -110,7 +110,7 @@ export type StructureToBallAndStick = StateTransform<StructureEntity, BallAndSti
 export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.create('structure', 'ballandstick', 'structure-to-ballandstick',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<BallAndStickProps> = {}) {
         const ballAndStickRepr = BallAndStickRepresentation()
-        await ballAndStickRepr.create(structureEntity.value, props).run(ctx.log)
+        await ballAndStickRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(ballAndStickRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -121,7 +121,7 @@ export type StructureToDistanceRestraint = StateTransform<StructureEntity, Dista
 export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateTransform.create('structure', 'distancerestraint', 'structure-to-distancerestraint',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<DistanceRestraintProps> = {}) {
         const distanceRestraintRepr = DistanceRestraintRepresentation()
-        await distanceRestraintRepr.create(structureEntity.value, props).run(ctx.log)
+        await distanceRestraintRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(distanceRestraintRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -132,7 +132,7 @@ export type StructureToBackbone = StateTransform<StructureEntity, BackboneEntity
 export const StructureToBackbone: StructureToBackbone = StateTransform.create('structure', 'backbone', 'structure-to-backbone',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<BackboneProps> = {}) {
         const backboneRepr = BackboneRepresentation()
-        await backboneRepr.create(structureEntity.value, props).run(ctx.log)
+        await backboneRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(backboneRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -143,7 +143,7 @@ export type StructureToCartoon = StateTransform<StructureEntity, CartoonEntity,
 export const StructureToCartoon: StructureToCartoon = StateTransform.create('structure', 'cartoon', 'structure-to-cartoon',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<CartoonProps> = {}) {
         const cartoonRepr = CartoonRepresentation()
-        await cartoonRepr.create(structureEntity.value, props).run(ctx.log)
+        await cartoonRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(cartoonRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -154,7 +154,7 @@ export type StructureToCarbohydrate = StateTransform<StructureEntity, Carbohydra
 export const StructureToCarbohydrate: StructureToCarbohydrate = StateTransform.create('structure', 'carbohydrate', 'structure-to-cartoon',
     async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<CarbohydrateProps> = {}) {
         const carbohydrateRepr = CarbohydrateRepresentation()
-        await carbohydrateRepr.create(structureEntity.value, props).run(ctx.log)
+        await carbohydrateRepr.createOrUpdate(props, structureEntity.value).run(ctx.log)
         ctx.viewer.add(carbohydrateRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -165,7 +165,7 @@ export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, Partia
 export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update',
     async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: Partial<SpacefillProps> = {}) {
         const spacefillRepr = spacefillEntity.value
-        await spacefillRepr.update(props).run(ctx.log)
+        await spacefillRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(spacefillRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -176,7 +176,7 @@ export type BallAndStickUpdate = StateTransform<BallAndStickEntity, NullEntity,
 export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('ballandstick', 'null', 'ballandstick-update',
     async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: Partial<BallAndStickProps> = {}) {
         const ballAndStickRepr = ballAndStickEntity.value
-        await ballAndStickRepr.update(props).run(ctx.log)
+        await ballAndStickRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(ballAndStickRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -187,7 +187,7 @@ export type DistanceRestraintUpdate = StateTransform<DistanceRestraintEntity, Nu
 export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.create('distancerestraint', 'null', 'distancerestraint-update',
     async function (ctx: StateContext, distanceRestraintEntity: DistanceRestraintEntity, props: Partial<DistanceRestraintProps> = {}) {
         const distanceRestraintRepr = distanceRestraintEntity.value
-        await distanceRestraintRepr.update(props).run(ctx.log)
+        await distanceRestraintRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(distanceRestraintRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -198,7 +198,7 @@ export type BackboneUpdate = StateTransform<BackboneEntity, NullEntity, Partial<
 export const BackboneUpdate: BackboneUpdate = StateTransform.create('backbone', 'null', 'backbone-update',
     async function (ctx: StateContext, backboneEntity: BackboneEntity, props: Partial<BackboneProps> = {}) {
         const backboneRepr = backboneEntity.value
-        await backboneRepr.update(props).run(ctx.log)
+        await backboneRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(backboneRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -209,7 +209,7 @@ export type CartoonUpdate = StateTransform<CartoonEntity, NullEntity, Partial<Ca
 export const CartoonUpdate: CartoonUpdate = StateTransform.create('cartoon', 'null', 'cartoon-update',
     async function (ctx: StateContext, cartoonEntity: CartoonEntity, props: Partial<CartoonProps> = {}) {
         const cartoonRepr = cartoonEntity.value
-        await cartoonRepr.update(props).run(ctx.log)
+        await cartoonRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(cartoonRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
@@ -220,7 +220,7 @@ export type CarbohydrateUpdate = StateTransform<CarbohydrateEntity, NullEntity,
 export const CarbohydrateUpdate: CarbohydrateUpdate = StateTransform.create('carbohydrate', 'null', 'carbohydrate-update',
     async function (ctx: StateContext, carbohydrateEntity: CarbohydrateEntity, props: Partial<CarbohydrateProps> = {}) {
         const carbohydrateRepr = carbohydrateEntity.value
-        await carbohydrateRepr.update(props).run(ctx.log)
+        await carbohydrateRepr.createOrUpdate(props).run(ctx.log)
         ctx.viewer.add(carbohydrateRepr)
         ctx.viewer.requestDraw()
         console.log('stats', ctx.viewer.stats)
diff --git a/src/mol-view/theme/color.ts b/src/mol-view/theme/color.ts
index f02f54b3a11d757f1d636d71a99a12f8d414ee1f..7b1f0e4d2f6275d31d13506303a5a512db649a1a 100644
--- a/src/mol-view/theme/color.ts
+++ b/src/mol-view/theme/color.ts
@@ -6,7 +6,7 @@
 
 import { Color } from 'mol-util/color';
 import { Structure } from 'mol-model/structure';
-import { ColorType, LocationColor } from 'mol-geo/util/color-data';
+import { Location } from 'mol-model/location';
 
 import { ElementIndexColorTheme } from './color/element-index';
 import { CarbohydrateSymbolColorTheme } from './color/carbohydrate-symbol';
@@ -16,10 +16,34 @@ import { UnitIndexColorTheme } from './color/unit-index';
 import { UniformColorTheme } from './color/uniform';
 import { CrossLinkColorTheme } from './color/cross-link';
 import { ShapeGroupColorTheme } from './color/shape-group';
+import { CustomColorTheme } from './color/custom';
+import { ColorType } from 'mol-geo/util/color-data';
+
+export type LocationColor = (location: Location, isSecondary: boolean) => Color
+
+export interface ScaleLegend {
+    kind: 'scale-legend'
+    min: number,
+    max: number,
+    colors: Color[]
+}
+export function ScaleLegend(min: number, max: number, colors: Color[]): ScaleLegend {
+    return { kind: 'scale-legend', min, max, colors }
+}
+
+export interface TableLegend {
+    kind: 'table-legend'
+    table: [ string, Color ][]
+}
+export function TableLegend(table: [ string, Color ][]): TableLegend {
+    return { kind: 'table-legend', table }
+}
 
 export interface ColorTheme {
-    kind: ColorType
+    granularity: ColorType
     color: LocationColor
+    description?: string
+    legend?: ScaleLegend | TableLegend
 }
 
 export function ColorTheme(props: ColorThemeProps): ColorTheme {
@@ -32,6 +56,7 @@ export function ColorTheme(props: ColorThemeProps): ColorTheme {
         case 'unit-index': return UnitIndexColorTheme(props)
         case 'uniform': return UniformColorTheme(props)
         case 'shape-group': return ShapeGroupColorTheme(props)
+        case 'custom': return CustomColorTheme(props)
     }
 }
 
@@ -40,6 +65,10 @@ export interface ColorThemeProps {
     domain?: [number, number]
     value?: Color
     structure?: Structure
+    color?: LocationColor
+    granularity?: ColorType,
+    description?: string,
+    legend?: ScaleLegend | TableLegend
 }
 
 export const ColorThemeInfo = {
@@ -50,7 +79,8 @@ export const ColorThemeInfo = {
     'element-symbol': {},
     'unit-index': {},
     'uniform': {},
-    'shape-group': {}
+    'shape-group': {},
+    'custom': {}
 }
 export type ColorThemeName = keyof typeof ColorThemeInfo
 export const ColorThemeNames = Object.keys(ColorThemeInfo)
\ No newline at end of file
diff --git a/src/mol-view/theme/color/carbohydrate-symbol.ts b/src/mol-view/theme/color/carbohydrate-symbol.ts
index 7c3093a0b2ffce1e3e8ac4e865516bf8976f1ce7..c777c91d538684085041c23698ee19657fb18de7 100644
--- a/src/mol-view/theme/color/carbohydrate-symbol.ts
+++ b/src/mol-view/theme/color/carbohydrate-symbol.ts
@@ -6,16 +6,16 @@
 
 import { StructureElement, Link, ElementIndex, Unit } from 'mol-model/structure';
 
-import { SaccharideColors } from 'mol-model/structure/structure/carbohydrates/constants';
+import { SaccharideColors, MonosaccharidesColorTable } from 'mol-model/structure/structure/carbohydrates/constants';
 import { Location } from 'mol-model/location';
-import { ColorThemeProps, ColorTheme } from '../color';
-import { LocationColor } from 'mol-geo/util/color-data';
+import { ColorThemeProps, ColorTheme, LocationColor, TableLegend } from '../color';
 import { Color } from 'mol-util/color';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Assigns colors according to the Symbol Nomenclature for Glycans (SNFG).'
 
 export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme {
-    let colorFn: LocationColor
+    let color: LocationColor
 
     if (props.structure) {
         const { elements, getElementIndex, getAnomericCarbon } = props.structure.carbohydrates
@@ -30,7 +30,7 @@ export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme
             return DefaultColor
         }
 
-        colorFn = (location: Location, isSecondary: boolean) => {
+        color = (location: Location, isSecondary: boolean) => {
             if (isSecondary) {
                 return SaccharideColors.Secondary
             } else {
@@ -43,11 +43,13 @@ export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme
             return DefaultColor
         }
     } else {
-        colorFn = () => DefaultColor
+        color = () => DefaultColor
     }
 
     return {
-        kind: 'group',
-        color: colorFn
+        granularity: 'group',
+        color: color,
+        description: Description,
+        legend: TableLegend(MonosaccharidesColorTable)
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/chain-id.ts b/src/mol-view/theme/color/chain-id.ts
index 9e15eec0f430d12805b680c155c03033cc7223e8..e281dc9a20dc39fd36c3f937ec8920bb87e1f9b3 100644
--- a/src/mol-view/theme/color/chain-id.ts
+++ b/src/mol-view/theme/color/chain-id.ts
@@ -8,9 +8,10 @@ import { Unit, StructureProperties, StructureElement, Link } from 'mol-model/str
 
 import { ColorScale, Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
-import { ColorThemeProps, ColorTheme } from '../color';
+import { ColorThemeProps, ColorTheme, LocationColor } from '../color';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Gives every chain a color based on its `asym_id` value.'
 
 function getAsymId(unit: Unit): StructureElement.Property<string> {
     switch (unit.kind) {
@@ -23,27 +24,49 @@ function getAsymId(unit: Unit): StructureElement.Property<string> {
 }
 
 export function ChainIdColorTheme(props: ColorThemeProps): ColorTheme {
-    const l = StructureElement.create()
-
-    function colorFn(location: Location): Color {
-        if (StructureElement.isLocation(location)) {
-            const map = location.unit.model.properties.asymIdSerialMap
-            const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] })
-            const asym_id = getAsymId(location.unit)
-            return scale.color(map.get(asym_id(location)) || 0)
-        } else if (Link.isLocation(location)) {
-            const map = location.aUnit.model.properties.asymIdSerialMap
-            const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] })
-            const asym_id = getAsymId(location.aUnit)
-            l.unit = location.aUnit
-            l.element = location.aUnit.elements[location.aIndex]
-            return scale.color(map.get(asym_id(l)) || 0)
+    let color: LocationColor
+    let scale: ColorScale | undefined = undefined
+    // const table: [string, Color][] = []
+
+    if (props.structure) {
+        const l = StructureElement.create()
+        const { models } = props.structure
+        const asymIdSerialMap = new Map<string, number>()
+        let j = 0
+        for (let i = 0, il = models.length; i <il; ++i) {
+            models[i].properties.asymIdSerialMap.forEach((v, k) => {
+                if (!asymIdSerialMap.has(k)) {
+                    asymIdSerialMap.set(k, j)
+                    j += 1
+                }
+            })
+        }
+        scale = ColorScale.create({ domain: [ 0, asymIdSerialMap.size - 1 ] })
+        const scaleColor = scale.color
+
+        // asymIdSerialMap.forEach((v, k) => table.push([k, scaleColor(v)]))
+
+        color = (location: Location): Color => {
+            if (StructureElement.isLocation(location)) {
+                const asym_id = getAsymId(location.unit)
+                return scaleColor(asymIdSerialMap.get(asym_id(location)) || 0)
+            } else if (Link.isLocation(location)) {
+                const asym_id = getAsymId(location.aUnit)
+                l.unit = location.aUnit
+                l.element = location.aUnit.elements[location.aIndex]
+                return scaleColor(asymIdSerialMap.get(asym_id(l)) || 0)
+            }
+            return DefaultColor
         }
-        return DefaultColor
+    } else {
+        color = () => DefaultColor
     }
 
     return {
-        kind: 'group',
-        color: colorFn
+        granularity: 'group',
+        color,
+        description: Description,
+        // legend: scale ? TableLegend(table) : undefined
+        legend: scale ? scale.legend : undefined
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/cross-link.ts b/src/mol-view/theme/color/cross-link.ts
index 5a4709dc550531cafeb512eef140987399612dfd..6f8ab371e6ff957f71fd081850f25acc55b23ac0 100644
--- a/src/mol-view/theme/color/cross-link.ts
+++ b/src/mol-view/theme/color/cross-link.ts
@@ -8,11 +8,11 @@ import { Link } from 'mol-model/structure';
 
 import { Color, ColorScale, ColorBrewer } from 'mol-util/color';
 import { Location } from 'mol-model/location';
-import { ColorThemeProps, ColorTheme } from '../color';
-import { LocationColor } from 'mol-geo/util/color-data';
+import { ColorThemeProps, ColorTheme, LocationColor } from '../color';
 import { Vec3 } from 'mol-math/linear-algebra';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).'
 
 const distVecA = Vec3.zero(), distVecB = Vec3.zero()
 function linkDistance(link: Link.Location) {
@@ -22,27 +22,31 @@ function linkDistance(link: Link.Location) {
 }
 
 export function CrossLinkColorTheme(props: ColorThemeProps): ColorTheme {
-    let colorFn: LocationColor
+    let color: LocationColor
+    let scale: ColorScale | undefined = undefined
 
     if (props.structure) {
         const crosslinks = props.structure.crossLinkRestraints
-        const scale = ColorScale.create({ domain: [ -10, 10 ], colors: ColorBrewer.RdYlBu })
+        scale = ColorScale.create({ domain: [ -10, 10 ], colors: ColorBrewer.RdYlBu })
+        const scaleColor = scale.color
 
-        colorFn = (location: Location): Color => {
+        color = (location: Location): Color => {
             if (Link.isLocation(location)) {
                 const pairs = crosslinks.getPairs(location.aIndex, location.aUnit, location.bIndex, location.bUnit)
                 if (pairs) {
-                    return scale.color(linkDistance(location) - pairs[0].distanceThreshold)
+                    return scaleColor(linkDistance(location) - pairs[0].distanceThreshold)
                 }
             }
             return DefaultColor
         }
     } else {
-        colorFn = () => DefaultColor
+        color = () => DefaultColor
     }
 
     return {
-        kind: 'group',
-        color: colorFn
+        granularity: 'group',
+        color,
+        description: Description,
+        legend: scale ? scale.legend : undefined
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/custom.ts b/src/mol-view/theme/color/custom.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4901ac06f6860660a7cfb1edb9a1505d9e052c17
--- /dev/null
+++ b/src/mol-view/theme/color/custom.ts
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color } from 'mol-util/color';
+import { ColorThemeProps, ColorTheme } from '../color';
+import { defaults } from 'mol-util';
+
+const DefaultColor = Color(0xCCCCCC)
+
+export function CustomColorTheme(props: ColorThemeProps): ColorTheme {
+    const value = defaults(props.value, DefaultColor)
+    return {
+        granularity: defaults(props.granularity, 'uniform'),
+        color: defaults(props.color, () => value),
+        description: props.description,
+        legend: props.legend
+    }
+}
\ No newline at end of file
diff --git a/src/mol-view/theme/color/element-index.ts b/src/mol-view/theme/color/element-index.ts
index f6abd54f3cdee2a02e2531e660a9b93449d831ba..8ca2d273a0c5c30c2d16c323adefd77ed043295d 100644
--- a/src/mol-view/theme/color/element-index.ts
+++ b/src/mol-view/theme/color/element-index.ts
@@ -6,45 +6,51 @@
 
 import { ColorScale, Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
-import { StructureElement, Link, Unit } from 'mol-model/structure';
+import { StructureElement, Link } from 'mol-model/structure';
 import { OrderedSet } from 'mol-data/int';
-import { LocationColor } from 'mol-geo/util/color-data';
-import { ColorThemeProps, ColorTheme } from '../color';
+import { ColorThemeProps, ColorTheme, LocationColor } from '../color';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Gives every element (atom or coarse sphere/gaussian) a unique color based on the position (index) of the element in the list of elements in the structure.'
 
 export function ElementIndexColorTheme(props: ColorThemeProps): ColorTheme {
-    let colorFn: LocationColor
+    let color: LocationColor
+    let scale: ColorScale | undefined = undefined
 
     if (props.structure) {
         const { units } = props.structure
         const unitCount = units.length
         const cummulativeElementCount = new Map<number, number>()
+        const unitIdIndex = new Map<number, number>()
 
         let elementCount = 0
         for (let i = 0; i < unitCount; ++i) {
             cummulativeElementCount.set(i, elementCount)
             elementCount += units[i].elements.length
+            unitIdIndex.set(units[i].id, i)
         }
-        const scale = ColorScale.create({ domain: [ 0, elementCount ] })
+        scale = ColorScale.create({ domain: [ 0, elementCount - 1 ] })
+        const scaleColor = scale.color
 
-        colorFn = (location: Location): Color => {
+        color = (location: Location): Color => {
             if (StructureElement.isLocation(location)) {
-                const unitIndex = Unit.findUnitById(location.unit.id, units)
+                const unitIndex = unitIdIndex.get(location.unit.id)!
                 const unitElementIndex = OrderedSet.findPredecessorIndex(location.unit.elements, location.element)
-                return scale.color(cummulativeElementCount.get(unitIndex) || 0 + unitElementIndex)
+                return scaleColor(cummulativeElementCount.get(unitIndex)! + unitElementIndex)
             } else if (Link.isLocation(location)) {
-                const unitId = Unit.findUnitById(location.aUnit.id, units)
-                return scale.color(cummulativeElementCount.get(unitId) || 0 + location.aIndex)
+                const unitIndex = unitIdIndex.get(location.aUnit.id)!
+                return scaleColor(cummulativeElementCount.get(unitIndex)! + location.aIndex)
             }
             return DefaultColor
         }
     } else {
-        colorFn = () => DefaultColor
+        color = () => DefaultColor
     }
 
     return {
-        kind: 'groupInstance',
-        color: colorFn
+        granularity: 'groupInstance',
+        color,
+        description: Description,
+        legend: scale ? scale.legend : undefined
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/element-symbol.ts b/src/mol-view/theme/color/element-symbol.ts
index eaa64ebb687eff4b8b8d2d9888ff5215a298eba6..c73ae2d46280e1491535b2d6d79c98a328760d49 100644
--- a/src/mol-view/theme/color/element-symbol.ts
+++ b/src/mol-view/theme/color/element-symbol.ts
@@ -8,14 +8,15 @@ import { ElementSymbol } from 'mol-model/structure/model/types';
 import { Color, ColorMap } from 'mol-util/color';
 import { StructureElement, Unit, Link } from 'mol-model/structure';
 import { Location } from 'mol-model/location';
-import { ColorThemeProps, ColorTheme } from '../color';
+import { ColorThemeProps, ColorTheme, TableLegend } from '../color';
 
 // from Jmol http://jmol.sourceforge.net/jscolors/ (or 0xFFFFFF)
 export const ElementSymbolColors = ColorMap({
-    'H': 0xFFFFFF, 'HE': 0xD9FFFF, 'LI': 0xCC80FF, 'BE': 0xC2FF00, 'B': 0xFFB5B5, 'C': 0x909090, 'N': 0x3050F8, 'O': 0xFF0D0D, 'F': 0x90E050, 'NE': 0xB3E3F5, 'NA': 0xAB5CF2, 'MG': 0x8AFF00, 'AL': 0xBFA6A6, 'SI': 0xF0C8A0, 'P': 0xFF8000, 'S': 0xFFFF30, 'CL': 0x1FF01F, 'AR': 0x80D1E3, 'K': 0x8F40D4, 'CA': 0x3DFF00, 'SC': 0xE6E6E6, 'TI': 0xBFC2C7, 'V': 0xA6A6AB, 'CR': 0x8A99C7, 'MN': 0x9C7AC7, 'FE': 0xE06633, 'CO': 0xF090A0, 'NI': 0x50D050, 'CU': 0xC88033, 'ZN': 0x7D80B0, 'GA': 0xC28F8F, 'GE': 0x668F8F, 'AS': 0xBD80E3, 'SE': 0xFFA100, 'BR': 0xA62929, 'KR': 0x5CB8D1, 'RB': 0x702EB0, 'SR': 0x00FF00, 'Y': 0x94FFFF, 'ZR': 0x94E0E0, 'NB': 0x73C2C9, 'MO': 0x54B5B5, 'TC': 0x3B9E9E, 'RU': 0x248F8F, 'RH': 0x0A7D8C, 'PD': 0x006985, 'AG': 0xC0C0C0, 'CD': 0xFFD98F, 'IN': 0xA67573, 'SN': 0x668080, 'SB': 0x9E63B5, 'TE': 0xD47A00, 'I': 0x940094, 'XE': 0x940094, 'CS': 0x57178F, 'BA': 0x00C900, 'LA': 0x70D4FF, 'CE': 0xFFFFC7, 'PR': 0xD9FFC7, 'ND': 0xC7FFC7, 'PM': 0xA3FFC7, 'SM': 0x8FFFC7, 'EU': 0x61FFC7, 'GD': 0x45FFC7, 'TB': 0x30FFC7, 'DY': 0x1FFFC7, 'HO': 0x00FF9C, 'ER': 0x00E675, 'TM': 0x00D452, 'YB': 0x00BF38, 'LU': 0x00AB24, 'HF': 0x4DC2FF, 'TA': 0x4DA6FF, 'W': 0x2194D6, 'RE': 0x267DAB, 'OS': 0x266696, 'IR': 0x175487, 'PT': 0xD0D0E0, 'AU': 0xFFD123, 'HG': 0xB8B8D0, 'TL': 0xA6544D, 'PB': 0x575961, 'BI': 0x9E4FB5, 'PO': 0xAB5C00, 'AT': 0x754F45, 'RN': 0x428296, 'FR': 0x420066, 'RA': 0x007D00, 'AC': 0x70ABFA, 'TH': 0x00BAFF, 'PA': 0x00A1FF, 'U': 0x008FFF, 'NP': 0x0080FF, 'PU': 0x006BFF, 'AM': 0x545CF2, 'CM': 0x785CE3, 'BK': 0x8A4FE3, 'CF': 0xA136D4, 'ES': 0xB31FD4, 'FM': 0xB31FBA, 'MD': 0xB30DA6, 'NO': 0xBD0D87, 'LR': 0xC70066, 'RF': 0xCC0059, 'DB': 0xD1004F, 'SG': 0xD90045, 'BH': 0xE00038, 'HS': 0xE6002E, 'MT': 0xEB0026, 'DS': 0xFFFFFF, 'RG': 0xFFFFFF, 'CN': 0xFFFFFF, 'UUT': 0xFFFFFF, 'FL': 0xFFFFFF, 'UUP': 0xFFFFFF, 'LV': 0xFFFFFF, 'UUH': 0xFFFFFF, 'D': 0xFFFFC0, 'T': 0xFFFFA0
+    'H': 0xFFFFFF, 'D': 0xFFFFC0, 'T': 0xFFFFA0, 'HE': 0xD9FFFF, 'LI': 0xCC80FF, 'BE': 0xC2FF00, 'B': 0xFFB5B5, 'C': 0x909090, 'N': 0x3050F8, 'O': 0xFF0D0D, 'F': 0x90E050, 'NE': 0xB3E3F5, 'NA': 0xAB5CF2, 'MG': 0x8AFF00, 'AL': 0xBFA6A6, 'SI': 0xF0C8A0, 'P': 0xFF8000, 'S': 0xFFFF30, 'CL': 0x1FF01F, 'AR': 0x80D1E3, 'K': 0x8F40D4, 'CA': 0x3DFF00, 'SC': 0xE6E6E6, 'TI': 0xBFC2C7, 'V': 0xA6A6AB, 'CR': 0x8A99C7, 'MN': 0x9C7AC7, 'FE': 0xE06633, 'CO': 0xF090A0, 'NI': 0x50D050, 'CU': 0xC88033, 'ZN': 0x7D80B0, 'GA': 0xC28F8F, 'GE': 0x668F8F, 'AS': 0xBD80E3, 'SE': 0xFFA100, 'BR': 0xA62929, 'KR': 0x5CB8D1, 'RB': 0x702EB0, 'SR': 0x00FF00, 'Y': 0x94FFFF, 'ZR': 0x94E0E0, 'NB': 0x73C2C9, 'MO': 0x54B5B5, 'TC': 0x3B9E9E, 'RU': 0x248F8F, 'RH': 0x0A7D8C, 'PD': 0x006985, 'AG': 0xC0C0C0, 'CD': 0xFFD98F, 'IN': 0xA67573, 'SN': 0x668080, 'SB': 0x9E63B5, 'TE': 0xD47A00, 'I': 0x940094, 'XE': 0x940094, 'CS': 0x57178F, 'BA': 0x00C900, 'LA': 0x70D4FF, 'CE': 0xFFFFC7, 'PR': 0xD9FFC7, 'ND': 0xC7FFC7, 'PM': 0xA3FFC7, 'SM': 0x8FFFC7, 'EU': 0x61FFC7, 'GD': 0x45FFC7, 'TB': 0x30FFC7, 'DY': 0x1FFFC7, 'HO': 0x00FF9C, 'ER': 0x00E675, 'TM': 0x00D452, 'YB': 0x00BF38, 'LU': 0x00AB24, 'HF': 0x4DC2FF, 'TA': 0x4DA6FF, 'W': 0x2194D6, 'RE': 0x267DAB, 'OS': 0x266696, 'IR': 0x175487, 'PT': 0xD0D0E0, 'AU': 0xFFD123, 'HG': 0xB8B8D0, 'TL': 0xA6544D, 'PB': 0x575961, 'BI': 0x9E4FB5, 'PO': 0xAB5C00, 'AT': 0x754F45, 'RN': 0x428296, 'FR': 0x420066, 'RA': 0x007D00, 'AC': 0x70ABFA, 'TH': 0x00BAFF, 'PA': 0x00A1FF, 'U': 0x008FFF, 'NP': 0x0080FF, 'PU': 0x006BFF, 'AM': 0x545CF2, 'CM': 0x785CE3, 'BK': 0x8A4FE3, 'CF': 0xA136D4, 'ES': 0xB31FD4, 'FM': 0xB31FBA, 'MD': 0xB30DA6, 'NO': 0xBD0D87, 'LR': 0xC70066, 'RF': 0xCC0059, 'DB': 0xD1004F, 'SG': 0xD90045, 'BH': 0xE00038, 'HS': 0xE6002E, 'MT': 0xEB0026, 'DS': 0xFFFFFF, 'RG': 0xFFFFFF, 'CN': 0xFFFFFF, 'UUT': 0xFFFFFF, 'FL': 0xFFFFFF, 'UUP': 0xFFFFFF, 'LV': 0xFFFFFF, 'UUH': 0xFFFFFF
 })
 
-const DefaultElementSymbolColor = 0xFFFFFF as Color
+const DefaultElementSymbolColor = Color(0xFFFFFF)
+const Description = 'Assigns a color to every atom according to its chemical element.'
 
 export function elementSymbolColor(element: ElementSymbol): Color {
     const c = (ElementSymbolColors as { [k: string]: Color })[element];
@@ -23,7 +24,7 @@ export function elementSymbolColor(element: ElementSymbol): Color {
 }
 
 export function ElementSymbolColorTheme(props: ColorThemeProps): ColorTheme {
-    function colorFn(location: Location): Color {
+    function color(location: Location): Color {
         if (StructureElement.isLocation(location)) {
             if (Unit.isAtomic(location.unit)) {
                 const { type_symbol } = location.unit.model.atomicHierarchy.atoms
@@ -39,7 +40,11 @@ export function ElementSymbolColorTheme(props: ColorThemeProps): ColorTheme {
     }
 
     return {
-        kind: 'group',
-        color: colorFn
+        granularity: 'group',
+        color,
+        description: Description,
+        legend: TableLegend(Object.keys(ElementSymbolColors).map(name => {
+            return [name, (ElementSymbolColors as any)[name] as Color] as [string, Color]
+        }))
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/shape-group.ts b/src/mol-view/theme/color/shape-group.ts
index f7be8107b23ea16e631fb08bd567e8af292b42e7..6743791a3c58d823359eab0f6f8052c1bb8b55aa 100644
--- a/src/mol-view/theme/color/shape-group.ts
+++ b/src/mol-view/theme/color/shape-group.ts
@@ -9,16 +9,18 @@ import { Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
 import { Shape } from 'mol-model/shape';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
 
 export function ShapeGroupColorTheme(props: ColorThemeProps): ColorTheme {
     return {
-        kind: 'group',
+        granularity: 'group',
         color: (location: Location): Color => {
             if (Shape.isLocation(location)) {
                 return location.shape.colors.ref.value[location.group]
             }
             return DefaultColor
-        }
+        },
+        description: props.description,
+        legend: props.legend
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/uniform.ts b/src/mol-view/theme/color/uniform.ts
index 7236e88fba49d5bf97ea2ddb9ae5d9be28534992..d9ab15c4db1cae6a21d6dd7b3376d3cd87817ce1 100644
--- a/src/mol-view/theme/color/uniform.ts
+++ b/src/mol-view/theme/color/uniform.ts
@@ -4,16 +4,19 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ColorTheme, ColorThemeProps } from '../color';
+import { ColorTheme, ColorThemeProps, TableLegend } from '../color';
 import { Color } from 'mol-util/color';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Gives everything the same, uniform color.'
 
 export function UniformColorTheme(props: ColorThemeProps): ColorTheme {
     const color = props.value || DefaultColor
 
     return {
-        kind: 'uniform',
-        color: () => color
+        granularity: 'uniform',
+        color: () => color,
+        description: Description,
+        legend: TableLegend([['uniform', color]])
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/color/unit-index.ts b/src/mol-view/theme/color/unit-index.ts
index f980de05b69f3747972679297a3eccd76499001a..92d4f53bbe552f2017b873dd7778ee0d6621e3e2 100644
--- a/src/mol-view/theme/color/unit-index.ts
+++ b/src/mol-view/theme/color/unit-index.ts
@@ -6,35 +6,40 @@
 
 import { ColorScale, Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
-import { Unit, StructureElement, Link } from 'mol-model/structure';
-import { LocationColor } from 'mol-geo/util/color-data';
-import { ColorTheme, ColorThemeProps } from '../color';
+import { StructureElement, Link } from 'mol-model/structure';
+import { ColorTheme, ColorThemeProps, LocationColor } from '../color';
 
-const DefaultColor = 0xCCCCCC as Color
+const DefaultColor = Color(0xCCCCCC)
+const Description = 'Gives every unit (single chain or collection of single elements) a unique color based on the position (index) of the unit in the list of units in the structure.'
 
 export function UnitIndexColorTheme(props: ColorThemeProps): ColorTheme {
-    let colorFn: LocationColor
+    let color: LocationColor
+    let scale: ColorScale | undefined = undefined
 
     if (props.structure) {
         const { units } = props.structure
-        const unitCount = units.length
-
-        const scale = ColorScale.create({ domain: [ 0, unitCount ] })
+        scale = ColorScale.create({ domain: [ 0, units.length - 1 ] })
+        const unitIdColor = new Map<number, Color>()
+        for (let i = 0, il = units.length; i <il; ++i) {
+            unitIdColor.set(units[i].id, scale.color(units[i].id))
+        }
 
-        colorFn = (location: Location): Color => {
+        color = (location: Location): Color => {
             if (StructureElement.isLocation(location)) {
-                return scale.color(Unit.findUnitById(location.unit.id, units))
+                return unitIdColor.get(location.unit.id)!
             } else if (Link.isLocation(location)) {
-                return scale.color(Unit.findUnitById(location.aUnit.id, units))
+                return unitIdColor.get(location.aUnit.id)!
             }
             return DefaultColor
         }
     } else {
-        colorFn = () => DefaultColor
+        color = () => DefaultColor
     }
 
     return {
-        kind: 'instance',
-        color: colorFn
+        granularity: 'instance',
+        color,
+        description: Description,
+        legend: scale ? scale.legend : undefined
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/size.ts b/src/mol-view/theme/size.ts
index 059c8b04fbf5a4d3bd767c1c8e04cd86e490a372..f1d829f4e0af499d2bee5196ed9c0d4f3f5360d0 100644
--- a/src/mol-view/theme/size.ts
+++ b/src/mol-view/theme/size.ts
@@ -11,7 +11,7 @@ import { PhysicalSizeTheme } from './size/physical';
 import { UniformSizeTheme } from './size/uniform';
 
 export interface SizeTheme {
-    kind: SizeType
+    granularity: SizeType
     size: LocationSize
 }
 
diff --git a/src/mol-view/theme/size/physical.ts b/src/mol-view/theme/size/physical.ts
index 87c7ccefe0c8ebe717d9269bf032fa077d7b7668..43a885989c0faf14da2a57fbc8b3f02eabc2aede 100644
--- a/src/mol-view/theme/size/physical.ts
+++ b/src/mol-view/theme/size/physical.ts
@@ -42,7 +42,7 @@ export function PhysicalSizeTheme(props: SizeThemeProps): SizeTheme {
     }
 
     return {
-        kind: 'group',
+        granularity: 'group',
         size: sizeFn
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/theme/size/uniform.ts b/src/mol-view/theme/size/uniform.ts
index f9810f2e72591b46a0dfd626a9863adc30f3f9bd..28634f5ee67867bd55df0f95c95158cce5a27dbf 100644
--- a/src/mol-view/theme/size/uniform.ts
+++ b/src/mol-view/theme/size/uniform.ts
@@ -15,7 +15,7 @@ export function UniformSizeTheme(props: SizeThemeProps): SizeTheme {
     const size = value * factor
 
     return {
-        kind: 'uniform',
+        granularity: 'uniform',
         size: () => size
     }
 }
\ No newline at end of file
diff --git a/src/mol-view/viewer.ts b/src/mol-view/viewer.ts
index d3eb6d3d22ebb55237a9c35a776fe7b698307bde..4e4384cbcb6f44bbdd38c4fe0da4b64d5025b12f 100644
--- a/src/mol-view/viewer.ts
+++ b/src/mol-view/viewer.ts
@@ -38,10 +38,10 @@ interface Viewer {
     clear: () => void
 
     draw: (force?: boolean) => void
-    requestDraw: () => void
+    requestDraw: (force?: boolean) => void
     animate: () => void
     pick: () => void
-    identify: (x: number, y: number) => PickingId
+    identify: (x: number, y: number) => PickingId | undefined
     mark: (loci: Loci, action: MarkerAction) => void
     getLoci: (pickingId: PickingId) => Loci
 
@@ -99,12 +99,10 @@ namespace Viewer {
         const ctx = createContext(gl)
 
         const scene = Scene.create(ctx)
-        // const controls = TrackballControls.create(input, scene, {})
         const controls = TrackballControls.create(input, camera, {})
-        // const renderer = Renderer.create(ctx, camera, { clearColor: 0xFFFFFF })
         const renderer = Renderer.create(ctx, camera, { clearColor: Color(0x000000) })
 
-        const pickScale = 1 / 4
+        const pickScale = 1
         const pickWidth = Math.round(canvas.width * pickScale)
         const pickHeight = Math.round(canvas.height * pickScale)
         const objectPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
@@ -112,7 +110,9 @@ namespace Viewer {
         const groupPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
 
         let pickDirty = true
+        let isPicking = false
         let drawPending = false
+        let lastRenderTime = -1
         const prevProjectionView = Mat4.zero()
         const prevSceneView = Mat4.zero()
 
@@ -129,9 +129,16 @@ namespace Viewer {
         }
 
         function mark(loci: Loci, action: MarkerAction) {
-            reprMap.forEach((roSet, repr) => repr.mark(loci, action))
-            scene.update()
-            requestDraw()
+            let changed = false
+            reprMap.forEach((roSet, repr) => {
+                changed = repr.mark(loci, action) || changed
+            })
+            if (changed) {
+                // console.log('changed')
+                scene.update()
+                draw(true)
+                pickDirty = false // picking buffers should not have changed
+            }
         }
 
         let nearPlaneDelta = 0
@@ -143,6 +150,7 @@ namespace Viewer {
         }
 
         function render(variant: RenderVariant, force?: boolean) {
+            if (isPicking) return false
             // const p = scene.boundingSphere.center
             // console.log(p[0], p[1], p[2])
             // Vec3.set(controls.target, p[0], p[1], p[2])
@@ -156,7 +164,7 @@ namespace Viewer {
             let fogNear = targetDistance - camera.near + 1 * focusRadius - nearPlaneDelta;
             let fogFar = targetDistance - camera.near + 2 * focusRadius - nearPlaneDelta;
 
-            //console.log(fogNear, fogFar);
+            // console.log(fogNear, fogFar);
             camera.fogNear = Math.max(fogNear, 0.1);
             camera.fogFar = Math.max(fogFar, 0.2);
 
@@ -174,15 +182,14 @@ namespace Viewer {
             let didRender = false
             controls.update()
             camera.update()
-            scene.update()
             if (force || !Mat4.areEqual(camera.projectionView, prevProjectionView, EPSILON.Value) || !Mat4.areEqual(scene.view, prevSceneView, EPSILON.Value)) {
                 // console.log('foo', force, prevSceneView, scene.view)
                 Mat4.copy(prevProjectionView, camera.projectionView)
                 Mat4.copy(prevSceneView, scene.view)
                 renderer.render(scene, variant)
                 if (variant === 'draw') {
+                    lastRenderTime = performance.now()
                     pickDirty = true
-                    pick()
                 }
                 didRender = true
             }
@@ -196,14 +203,17 @@ namespace Viewer {
             drawPending = false
         }
 
-        function requestDraw () {
+        function requestDraw(force?: boolean) {
             if (drawPending) return
             drawPending = true
-            window.requestAnimationFrame(() => draw(true))
+            window.requestAnimationFrame(() => draw(force))
         }
 
-        function animate () {
+        function animate() {
             draw(false)
+            if (performance.now() - lastRenderTime > 200) {
+                if (pickDirty) pick()
+            }
             window.requestAnimationFrame(() => animate())
         }
 
@@ -215,7 +225,11 @@ namespace Viewer {
             pickDirty = false
         }
 
-        function identify (x: number, y: number): PickingId {
+        function identify(x: number, y: number): PickingId | undefined {
+            if (pickDirty) return undefined
+
+            isPicking = true
+
             x *= ctx.pixelRatio
             y *= ctx.pixelRatio
             y = canvas.height - y // flip y
@@ -236,6 +250,8 @@ namespace Viewer {
             ctx.readPixels(xp, yp, 1, 1, buffer)
             const groupId = decodeIdRGBA(buffer[0], buffer[1], buffer[2])
 
+            isPicking = false
+
             return { objectId, instanceId, groupId }
         }
 
@@ -268,6 +284,7 @@ namespace Viewer {
                 }
                 reprMap.set(repr, newRO)
                 reprCount.next(reprMap.size)
+                scene.update()
             },
             remove: (repr: Representation<any>) => {
                 const renderObjectSet = reprMap.get(repr)
@@ -275,6 +292,7 @@ namespace Viewer {
                     renderObjectSet.forEach(o => scene.remove(o))
                     reprMap.delete(repr)
                     reprCount.next(reprMap.size)
+                    scene.update()
                 }
             },
             update: () => scene.update(),