Browse Source

Merge branch 'canvas' of https://github.com/luna215/molstar-proto into canvas

luna215 6 years ago
parent
commit
6cf6740a0a
100 changed files with 1819 additions and 2635 deletions
  1. 0 2
      package.json
  2. 0 146
      src/apps/canvas/app.ts
  3. 0 99
      src/apps/canvas/assembly-symmetry.ts
  4. 0 130
      src/apps/canvas/component/app.tsx
  5. 0 68
      src/apps/canvas/component/representation.tsx
  6. 0 190
      src/apps/canvas/component/structure-view.tsx
  7. 0 187
      src/apps/canvas/component/viewport.tsx
  8. 0 105
      src/apps/canvas/component/volume-view.tsx
  9. 0 183
      src/apps/canvas/examples.ts
  10. 0 33
      src/apps/canvas/index.html
  11. 0 49
      src/apps/canvas/index.ts
  12. 0 369
      src/apps/canvas/structure-view.ts
  13. 0 78
      src/apps/canvas/util.ts
  14. 0 97
      src/apps/canvas/volume-view.ts
  15. 1 0
      src/mol-canvas3d/camera.ts
  16. 13 18
      src/mol-canvas3d/canvas3d.ts
  17. 2 2
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  18. 10 8
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  19. 0 5
      src/mol-geo/geometry/geometry.ts
  20. 7 5
      src/mol-geo/geometry/lines/lines.ts
  21. 46 57
      src/mol-geo/geometry/mesh/builder/sheet.ts
  22. 8 6
      src/mol-geo/geometry/mesh/mesh.ts
  23. 8 6
      src/mol-geo/geometry/points/points.ts
  24. 12 5
      src/mol-gl/scene.ts
  25. 1 1
      src/mol-gl/shader/chunks/apply-marker-color.glsl
  26. 14 5
      src/mol-gl/webgl/render-item.ts
  27. 11 1
      src/mol-gl/webgl/vertex-array.ts
  28. 124 0
      src/mol-math/geometry/boundary-helper.ts
  29. 22 2
      src/mol-math/geometry/lookup3d/grid.ts
  30. 0 77
      src/mol-math/geometry/primitives/sphere3d.ts
  31. 14 14
      src/mol-math/geometry/symmetry-operator.ts
  32. 28 0
      src/mol-model-props/pdbe/structure-quality-report.ts
  33. 54 0
      src/mol-model-props/pdbe/themes/structure-quality-report.ts
  34. 8 2
      src/mol-model/loci.ts
  35. 12 24
      src/mol-model/structure/model/formats/mmcif.ts
  36. 0 2
      src/mol-model/structure/model/model.ts
  37. 20 2
      src/mol-model/structure/query/queries/internal.ts
  38. 6 9
      src/mol-model/structure/structure/carbohydrates/compute.ts
  39. 11 1
      src/mol-model/structure/structure/carbohydrates/constants.ts
  40. 35 2
      src/mol-model/structure/structure/structure.ts
  41. 9 0
      src/mol-model/structure/structure/symmetry.ts
  42. 12 8
      src/mol-model/structure/structure/unit.ts
  43. 1 1
      src/mol-model/structure/structure/unit/gaussian-density.ts
  44. 35 77
      src/mol-model/structure/structure/util/boundary.ts
  45. 2 0
      src/mol-plugin/behavior.ts
  46. 1 1
      src/mol-plugin/behavior/behavior.ts
  47. 81 0
      src/mol-plugin/behavior/dynamic/custom-props.ts
  48. 4 4
      src/mol-plugin/behavior/dynamic/representation.ts
  49. 4 5
      src/mol-plugin/behavior/static/representation.ts
  50. 26 1
      src/mol-plugin/behavior/static/state.ts
  51. 2 0
      src/mol-plugin/command.ts
  52. 22 14
      src/mol-plugin/context.ts
  53. 7 5
      src/mol-plugin/index.ts
  54. 1 1
      src/mol-plugin/skin/base/components/controls-base.scss
  55. 51 0
      src/mol-plugin/skin/base/components/controls.scss
  56. 3 0
      src/mol-plugin/skin/base/components/temp.scss
  57. 1 0
      src/mol-plugin/skin/base/components/transformer.scss
  58. 152 71
      src/mol-plugin/state/actions/basic.ts
  59. 7 3
      src/mol-plugin/state/objects.ts
  60. 47 20
      src/mol-plugin/state/transforms/data.ts
  61. 84 66
      src/mol-plugin/state/transforms/model.ts
  62. 38 19
      src/mol-plugin/state/transforms/representation.ts
  63. 1 1
      src/mol-plugin/ui/controls.tsx
  64. 75 25
      src/mol-plugin/ui/controls/parameters.tsx
  65. 53 5
      src/mol-plugin/ui/controls/slider.tsx
  66. 18 16
      src/mol-plugin/ui/plugin.tsx
  67. 14 2
      src/mol-plugin/ui/state-tree.tsx
  68. 3 4
      src/mol-plugin/ui/state.tsx
  69. 2 1
      src/mol-plugin/ui/state/apply-action.tsx
  70. 22 4
      src/mol-plugin/ui/state/common.tsx
  71. 13 1
      src/mol-plugin/ui/state/update-transform.tsx
  72. 65 0
      src/mol-plugin/util/custom-prop-registry.ts
  73. 52 26
      src/mol-repr/representation.ts
  74. 17 11
      src/mol-repr/shape/representation.ts
  75. 19 14
      src/mol-repr/structure/complex-representation.ts
  76. 11 8
      src/mol-repr/structure/complex-visual.ts
  77. 6 8
      src/mol-repr/structure/representation/ball-and-stick.ts
  78. 6 9
      src/mol-repr/structure/representation/carbohydrate.ts
  79. 8 11
      src/mol-repr/structure/representation/cartoon.ts
  80. 7 32
      src/mol-repr/structure/representation/molecular-surface.ts
  81. 38 28
      src/mol-repr/structure/units-representation.ts
  82. 47 27
      src/mol-repr/structure/units-visual.ts
  83. 2 2
      src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts
  84. 3 3
      src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts
  85. 0 2
      src/mol-repr/structure/visual/element-sphere.ts
  86. 2 2
      src/mol-repr/structure/visual/nucleotide-block-mesh.ts
  87. 1 1
      src/mol-repr/structure/visual/polymer-direction-wedge.ts
  88. 4 4
      src/mol-repr/structure/visual/polymer-trace-mesh.ts
  89. 30 16
      src/mol-repr/structure/visual/util/polymer/trace-iterator.ts
  90. 3 0
      src/mol-repr/util.ts
  91. 14 5
      src/mol-repr/volume/direct-volume.ts
  92. 14 5
      src/mol-repr/volume/isosurface-mesh.ts
  93. 4 1
      src/mol-repr/volume/registry.ts
  94. 25 18
      src/mol-repr/volume/representation.ts
  95. 51 8
      src/mol-state/action.ts
  96. 4 3
      src/mol-state/object.ts
  97. 43 25
      src/mol-state/state.ts
  98. 69 7
      src/mol-state/transformer.ts
  99. 2 2
      src/mol-state/tree/builder.ts
  100. 14 12
      src/mol-theme/color.ts

+ 0 - 2
package.json

@@ -19,8 +19,6 @@
     "test": "jest",
     "build-viewer": "webpack build/node_modules/apps/viewer/index.js --mode development -o build/viewer/index.js",
     "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js",
-    "build-canvas": "webpack build/node_modules/apps/canvas/index.js --mode development -o build/canvas/index.js",
-    "watch-canvas": "webpack build/node_modules/apps/canvas/index.js -w --mode development -o build/canvas/index.js",
     "build-ms-query": "webpack build/node_modules/apps/model-server-query/index.js --mode development -o build/model-server-query/index.js",
     "watch-ms-query": "webpack build/node_modules/apps/model-server-query/index.js -w --mode development -o build/model-server-query/index.js",
     "model-server": "node build/node_modules/servers/model/server.js",

+ 0 - 146
src/apps/canvas/app.ts

@@ -1,146 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Canvas3D } from 'mol-canvas3d/canvas3d';
-import { getCifFromUrl, getModelsFromMmcif, getCifFromFile, getCcp4FromUrl, getVolumeFromCcp4, getCcp4FromFile, getVolumeFromVolcif } from './util';
-import { StructureView } from './structure-view';
-import { BehaviorSubject } from 'rxjs';
-import { CifBlock } from 'mol-io/reader/cif';
-import { VolumeView } from './volume-view';
-import { Ccp4File } from 'mol-io/reader/ccp4/schema';
-import { Progress } from 'mol-task';
-import { ColorTheme } from 'mol-theme/color';
-import { SizeTheme } from 'mol-theme/size';
-import { StructureRepresentationRegistry } from 'mol-repr/structure/registry';
-import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry';
-
-export class App {
-    canvas3d: Canvas3D
-    container: HTMLDivElement | null = null;
-    canvas: HTMLCanvasElement | null = null;
-    structureView: StructureView | null = null;
-    volumeView: VolumeView | null = null;
-
-    structureLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null)
-    volumeLoaded: BehaviorSubject<VolumeView | null> = new BehaviorSubject<VolumeView | null>(null)
-
-    colorThemeRegistry = new ColorTheme.Registry()
-    sizeThemeRegistry = new SizeTheme.Registry()
-    structureRepresentationRegistry = new StructureRepresentationRegistry()
-    volumeRepresentationRegistry = new VolumeRepresentationRegistry()
-
-    initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) {
-        this.canvas = _canvas
-        this.container = _container
-
-        try {
-            this.canvas3d = Canvas3D.create(this.canvas, this.container)
-            this.canvas3d.animate()
-            return true
-        } catch (e) {
-            console.error(e)
-            return false
-        }
-    }
-
-    setStatus(msg: string) {
-
-    }
-
-    private taskCount = 0
-    taskCountChanged = new BehaviorSubject({ count: 0, info: '' })
-
-    private changeTaskCount(delta: number, info = '') {
-        this.taskCount += delta
-        this.taskCountChanged.next({ count: this.taskCount, info })
-    }
-
-    async runTask<T>(promise: Promise<T>, info: string) {
-        this.changeTaskCount(1, info)
-        let result: T
-        try {
-            result = await promise
-        } finally {
-            this.changeTaskCount(-1)
-        }
-        return result
-    }
-
-    log(progress: Progress) {
-        console.log(Progress.format(progress))
-    }
-
-    get reprCtx () {
-        return {
-            webgl: this.canvas3d.webgl,
-            colorThemeRegistry: this.colorThemeRegistry,
-            sizeThemeRegistry: this.sizeThemeRegistry
-        }
-    }
-
-    //
-
-    async loadMmcif(cif: CifBlock, assemblyId?: string) {
-        const models = await this.runTask(getModelsFromMmcif(cif), 'Build models')
-        this.structureView = await this.runTask(StructureView(this, this.canvas3d, models, { assemblyId }), 'Init structure view')
-        this.structureLoaded.next(this.structureView)
-    }
-
-    async loadPdbIdOrMmcifUrl(idOrUrl: string, options?: { assemblyId?: string, binary?: boolean }) {
-        if (this.structureView) this.structureView.destroy();
-        const url = idOrUrl.length <= 4 ? `https://files.rcsb.org/download/${idOrUrl}.cif` : idOrUrl;
-        const cif = await this.runTask(getCifFromUrl(url, options ? !!options.binary : false), 'Load mmCIF from URL')
-        this.loadMmcif(cif.blocks[0], options ? options.assemblyId : void 0)
-    }
-
-    async loadMmcifFile(file: File) {
-        if (this.structureView) this.structureView.destroy();
-        const binary = /\.bcif$/.test(file.name);
-        const cif = await this.runTask(getCifFromFile(file, binary), 'Load mmCIF from file')
-        this.loadMmcif(cif.blocks[0])
-    }
-
-    //
-
-    async loadCcp4(ccp4: Ccp4File) {
-        const volume = await this.runTask(getVolumeFromCcp4(ccp4), 'Get Volume')
-        this.volumeView = await this.runTask(VolumeView(this, this.canvas3d, volume), 'Init volume view')
-        this.volumeLoaded.next(this.volumeView)
-    }
-
-    async loadCcp4File(file: File) {
-        if (this.volumeView) this.volumeView.destroy();
-        const ccp4 = await this.runTask(getCcp4FromFile(file), 'Load CCP4 from file')
-        this.loadCcp4(ccp4)
-    }
-
-    async loadCcp4Url(url: string) {
-        if (this.volumeView) this.volumeView.destroy();
-        const ccp4 = await this.runTask(getCcp4FromUrl(url), 'Load CCP4 from URL')
-        this.loadCcp4(ccp4)
-    }
-
-    //
-
-    async loadVolcif(cif: CifBlock) {
-        const volume = await this.runTask(getVolumeFromVolcif(cif), 'Get Volume')
-        this.volumeView = await this.runTask(VolumeView(this, this.canvas3d, volume), 'Init volume view')
-        this.volumeLoaded.next(this.volumeView)
-    }
-
-    async loadVolcifFile(file: File) {
-        if (this.volumeView) this.volumeView.destroy();
-        const binary = /\.bcif$/.test(file.name);
-        const cif = await this.runTask(getCifFromFile(file, binary), 'Load volCif from file')
-        this.loadVolcif(cif.blocks[1])
-    }
-
-    async loadVolcifUrl(url: string, binary?: boolean) {
-        if (this.volumeView) this.volumeView.destroy();
-        const cif = await this.runTask(getCifFromUrl(url, binary), 'Load volCif from URL')
-        this.loadVolcif(cif.blocks[1])
-    }
-}

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

@@ -1,99 +0,0 @@
-/**
- * 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/geometry/mesh/mesh-builder';
-import { Tensor } from 'mol-math/linear-algebra';
-import { addSphere } from 'mol-geo/geometry/mesh/builder/sphere';
-import { addCylinder } from 'mol-geo/geometry/mesh/builder/cylinder';
-import { Shape } from 'mol-model/shape';
-import { ColorTheme } from 'mol-theme/color';
-import { Location } from 'mol-model/location';
-import { StructureElement, Unit, StructureProperties } from 'mol-model/structure';
-
-export function getAxesShape(symmetryId: number, assemblySymmetry: AssemblySymmetry) {
-    const s = assemblySymmetry.db.rcsb_assembly_symmetry
-    const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId)
-    if (!symmetry) return
-
-    const axes = assemblySymmetry.getAxes(symmetryId)
-    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 ${symmetry.kind} ${symmetry.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.label_asym_id
-        case Unit.Kind.Spheres:
-        case Unit.Kind.Gaussians:
-            return StructureProperties.coarse.asym_id
-    }
-}
-
-function clusterMemberKey (asym_id: string, oper_list_ids: string[]) {
-    return `${asym_id}-${oper_list_ids.join('x')}`
-}
-
-export function getClusterColorTheme(symmetryId: number, assemblySymmetry: AssemblySymmetry): ColorTheme {
-    const DefaultColor = Color(0xCCCCCC)
-    const s = assemblySymmetry.db.rcsb_assembly_symmetry
-    const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId)
-    if (!symmetry) return { granularity: 'uniform', color: () => DefaultColor, props: {} }
-
-    const clusters = assemblySymmetry.getClusters(symmetryId)
-    if (!clusters._rowCount) return { granularity: 'uniform', color: () => DefaultColor, props: {} }
-
-    const clusterByMember = new Map<string, number>()
-    for (let i = 0, il = clusters._rowCount; i < il; ++i) {
-        const clusterMembers = assemblySymmetry.getClusterMembers(clusters.id.value(i))
-        for (let j = 0, jl = clusterMembers._rowCount; j < jl; ++j) {
-            const asym_id = clusterMembers.asym_id.value(j)
-            const oper_list_ids = clusterMembers.pdbx_struct_oper_list_ids.value(j)
-            clusterByMember.set(clusterMemberKey(asym_id, oper_list_ids), i)
-        }
-    }
-    const scale = ColorScale.create({ domain: [ 0, clusters._rowCount - 1 ] })
-
-    return {
-        granularity: 'instance',
-        color: (location: Location): Color => {
-            if (StructureElement.isLocation(location)) {
-                const asym_id = getAsymId(location.unit)
-                const ns = location.unit.conformation.operator.name.split('-')
-                const oper_list_ids = ns.length === 2 ? ns[1].split('x') : []
-                const cluster = clusterByMember.get(clusterMemberKey(asym_id(location), oper_list_ids))
-                return cluster !== undefined ? scale.color(cluster) : DefaultColor
-            }
-            return DefaultColor
-        },
-        props: {}
-    }
-}

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

@@ -1,130 +0,0 @@
-/**
- * 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';
-import { Examples } from '../examples';
-import { VolumeViewComponent } from './volume-view';
-import { VolumeView } from '../volume-view';
-
-export interface AppProps {
-    app: App
-}
-
-export interface AppState {
-    structureView: StructureView | null,
-    volumeView: VolumeView | null,
-    mmcifBinary: boolean,
-    volcifBinary: boolean
-}
-
-export class AppComponent extends React.Component<AppProps, AppState> {
-    state = {
-        structureView: this.props.app.structureView,
-        volumeView: this.props.app.volumeView,
-        mmcifBinary: false,
-        volcifBinary: true
-    }
-
-    componentDidMount() {
-        this.props.app.structureLoaded.subscribe((structureView) => {
-            this.setState({ structureView: this.props.app.structureView })
-        })
-        this.props.app.volumeLoaded.subscribe((volumeView) => {
-            this.setState({ volumeView: this.props.app.volumeView })
-        })
-    }
-
-    render() {
-        const { structureView, volumeView } = 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 or URL</span>&nbsp;&nbsp;
-                    <input type='checkbox' checked={this.state.mmcifBinary} onChange={e => this.setState({ mmcifBinary: e.target.checked })} /> Binary<br />
-                    <input
-                        style={{ width: '100%' }}
-                        type='text'
-                        onKeyDown={e => {
-                            if (e.keyCode === 13) {
-                                const value = e.currentTarget.value.trim()
-                                if (value) {
-                                    this.props.app.loadPdbIdOrMmcifUrl(value, { binary: this.state.mmcifBinary })
-                                }
-                            }
-                        }}
-                    />
-                </div>
-                <div>
-                    <span>Load CIF file </span>
-                    <input
-                        accept='*.cif'
-                        type='file'
-                        onChange={e => {
-                            if (e.target.files) this.props.app.loadMmcifFile(e.target.files[0])
-                        }}
-                    />
-                </div>
-                <div>
-                    <span>Load CCP4/MRC file </span>
-                    <input
-                        accept='*.ccp4,*.mrc, *.map'
-                        type='file'
-                        onChange={e => {
-                            if (e.target.files) this.props.app.loadCcp4File(e.target.files[0])
-                        }}
-                    />
-                </div>
-                <div style={{marginTop: '10px'}}>
-                    <span>Load DensityServer URL</span>&nbsp;&nbsp;
-                    <input type='checkbox' checked={this.state.volcifBinary} onChange={e => this.setState({ volcifBinary: e.target.checked })} /> Binary<br />
-                    <input
-                        style={{ width: '100%' }}
-                        type='text'
-                        onKeyDown={e => {
-                            if (e.keyCode === 13) {
-                                const value = e.currentTarget.value.trim()
-                                if (value) {
-                                    this.props.app.loadVolcifUrl(value, this.state.volcifBinary)
-                                }
-                            }
-                        }}
-                    />
-                </div>
-                <div>
-                    <span>Load example </span>
-                    <select
-                        style={{width: '200px'}}
-                        onChange={e => {
-                            this.props.app.loadPdbIdOrMmcifUrl(e.target.value)
-                        }}
-                    >
-                        <option value=''></option>
-                        {Examples.map(({label, id, description}, i) => {
-                            return <option key={i} value={id}>{`${label ? label : id} - ${description}`}</option>
-                        })}
-                    </select>
-                </div>
-                <hr/>
-                <div style={{marginBottom: '10px'}}>
-                    {structureView ? <StructureViewComponent structureView={structureView} /> : ''}
-                </div>
-                <hr/>
-                <div style={{marginBottom: '10px'}}>
-                    {volumeView ? <VolumeViewComponent volumeView={volumeView} /> : ''}
-                </div>
-            </div>
-        </div>;
-    }
-}

+ 0 - 68
src/apps/canvas/component/representation.tsx

@@ -1,68 +0,0 @@
-/**
- * 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 { Canvas3D } from 'mol-canvas3d/canvas3d';
-import { App } from '../app';
-import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { Representation } from 'mol-repr/representation';
-import { ParametersComponent } from 'mol-app/component/parameters';
-
-export interface RepresentationComponentProps<P extends PD.Params> {
-    app: App
-    canvas3d: Canvas3D
-    repr: Representation<P>
-}
-
-export interface RepresentationComponentState {
-    label: string
-    reprParams: PD.Params
-    reprProps: Readonly<{}>
-}
-
-export class RepresentationComponent<P extends PD.Params> extends React.Component<RepresentationComponentProps<P>, RepresentationComponentState> {
-
-    private stateFromRepr(repr: Representation<P>) {
-        return {
-            label: repr.label,
-            reprParams: repr.params,
-            reprProps: repr.props
-        }
-    }
-
-    componentWillMount() {
-        this.setState(this.stateFromRepr(this.props.repr))
-    }
-
-    async onChange(k: string, v: any) {
-        await this.props.app.runTask(this.props.repr.createOrUpdate(this.props.app.reprCtx, { [k]: v }).run(
-            progress => this.props.app.log(progress)
-        ), 'Representation Update')
-        this.setState(this.stateFromRepr(this.props.repr))
-    }
-
-    render() {
-        const { label, reprParams, reprProps } = this.state
-        // let colorTheme: ColorTheme | undefined = undefined
-        // if ('colorTheme' in reprProps) {
-        //     colorTheme = ColorTheme(getColorThemeProps(reprProps))
-        // }
-
-        return <div>
-            <div>
-                <h4>{label}</h4>
-            </div>
-            <div>
-                <ParametersComponent
-                    params={reprParams}
-                    values={reprProps}
-                    onChange={(k, v) => this.onChange(k as string, v)}
-                />
-            </div>
-            {/* { colorTheme !== undefined ? <ColorThemeComponent colorTheme={colorTheme} /> : '' } */}
-        </div>;
-    }
-}

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

@@ -1,190 +0,0 @@
-/**
- * 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 { RepresentationComponent } from './representation';
-import { Representation } from 'mol-repr/representation';
-import { StructureRepresentation } from 'mol-repr/structure/representation';
-
-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,
-            structure: sv.structure,
-            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'
-                        defaultValue={this.state.modelId.toString()}
-                        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}>
-                                <RepresentationComponent
-                                    repr={structureRepresentations[k] as Representation<any>}
-                                    canvas3d={structureView.canvas3d}
-                                    app={structureView.app}
-                                />
-                            </div>
-                        } else {
-                            return ''
-                        }
-                    } ) }
-                </div>
-            </div>
-        </div>;
-    }
-}

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

@@ -1,187 +0,0 @@
-/**
- * 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/geometry/marker-data';
-import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci';
-import { labelFirst } from 'mol-theme/label';
-import { ButtonsType } from 'mol-util/input/input-observer';
-import { throttleTime } from 'rxjs/operators'
-import { Camera } from 'mol-canvas3d/camera';
-import { ColorParamComponent } from 'mol-app/component/parameter/color';
-import { Color } from 'mol-util/color';
-import { ParamDefinition as PD } from 'mol-util/param-definition'
-
-interface ViewportProps {
-    app: App
-}
-
-interface ViewportState {
-    noWebGl: boolean
-    pickingInfo: string
-    taskInfo: string
-    cameraMode: Camera.Mode
-    backgroundColor: Color
-}
-
-const BackgroundColorParam = PD.Color(Color(0x000000), { label: 'Background Color' })
-
-export class Viewport extends React.Component<ViewportProps, ViewportState> {
-    private container: HTMLDivElement | null = null;
-    private canvas: HTMLCanvasElement | null = null;
-
-    state: ViewportState = {
-        noWebGl: false,
-        pickingInfo: '',
-        taskInfo: '',
-        cameraMode: 'perspective',
-        backgroundColor: Color(0x000000)
-    };
-
-    handleResize() {
-        this.props.app.canvas3d.handleResize()
-    }
-
-    componentDidMount() {
-        if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) {
-            this.setState({ noWebGl: true });
-        }
-        this.handleResize()
-
-        this.setState({
-            cameraMode: this.props.app.canvas3d.props.cameraMode,
-            backgroundColor: this.props.app.canvas3d.props.backgroundColor
-        })
-
-        const canvas3d = this.props.app.canvas3d
-
-        canvas3d.input.resize.subscribe(() => this.handleResize())
-
-        let prevHighlightLoci: Loci = EmptyLoci
-        // TODO can the 'only ever have one extra element in the queue' functionality be done with rxjs?
-        let highlightQueueLength = 0
-        canvas3d.input.move.pipe(throttleTime(50)).subscribe(async ({x, y, inside, buttons}) => {
-            if (!inside || buttons || highlightQueueLength > 2) return
-            ++highlightQueueLength
-            const p = await canvas3d.identify(x, y)
-            --highlightQueueLength
-            if (p) {
-                const { loci } = canvas3d.getLoci(p)
-
-                if (!areLociEqual(loci, prevHighlightLoci)) {
-                    canvas3d.mark(prevHighlightLoci, MarkerAction.RemoveHighlight)
-                    canvas3d.mark(loci, MarkerAction.Highlight)
-                    prevHighlightLoci = loci
-
-                    const label = labelFirst(loci)
-                    const pickingInfo = `${label}`
-                    this.setState({ pickingInfo })
-                }
-            }
-        })
-
-        canvas3d.input.click.subscribe(async ({x, y, buttons}) => {
-            if (buttons !== ButtonsType.Flag.Primary) return
-            const p = await canvas3d.identify(x, y)
-            if (p) {
-                const { loci } = canvas3d.getLoci(p)
-                canvas3d.mark(loci, MarkerAction.Toggle)
-            }
-        })
-
-        this.props.app.taskCountChanged.subscribe(({ count, info }) => {
-            this.setState({ taskInfo: count > 0 ? 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.pickingInfo}
-            </div>
-            <div
-                style={{
-                    position: 'absolute',
-                    bottom: 10,
-                    right: 10,
-                    padding: 10,
-                    color: 'lightgrey',
-                    background: 'rgba(0, 0, 0, 0.2)'
-                }}
-            >
-                <div>
-                    <span>Camera Mode </span>
-                    <select
-                        value={this.state.cameraMode}
-                        style={{width: '150'}}
-                        onChange={e => {
-                            const p = { cameraMode: e.target.value as Camera.Mode }
-                            this.props.app.canvas3d.setProps(p)
-                            this.setState(p)
-                        }}
-                    >
-                        <option value='perspective'>Perspective</option>
-                        <option value='orthographic'>Orthographic</option>
-                    </select>
-                </div>
-                <ColorParamComponent
-                    label={BackgroundColorParam.label || ''}
-                    param={BackgroundColorParam}
-                    value={this.state.backgroundColor}
-                    onChange={value => {
-                        const p = { backgroundColor: value }
-                        this.props.app.canvas3d.setProps(p)
-                        this.setState(p)
-                    }}
-                />
-            </div>
-            { this.state.taskInfo ?
-                <div
-                    style={{
-                        position: 'absolute',
-                        top: 10,
-                        right: 10,
-                        padding: 10,
-                        color: 'lightgrey',
-                        background: 'rgba(0, 0, 0, 0.2)'
-                    }}
-                >
-                    {this.state.taskInfo}
-                </div>
-            : '' }
-        </div>;
-    }
-}

+ 0 - 105
src/apps/canvas/component/volume-view.tsx

@@ -1,105 +0,0 @@
-/**
- * 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 { RepresentationComponent } from './representation';
-import { Representation } from 'mol-repr/representation';
-import { VolumeView } from '../volume-view';
-import { VolumeRepresentation } from 'mol-repr/volume/representation';
-
-export interface VolumeViewComponentProps {
-    volumeView: VolumeView
-}
-
-export interface VolumeViewComponentState {
-    volumeView: VolumeView
-    label: string
-    active: { [k: string]: boolean }
-    volumeRepresentations: { [k: string]: VolumeRepresentation<any> }
-}
-
-export class VolumeViewComponent extends React.Component<VolumeViewComponentProps, VolumeViewComponentState> {
-    state = this.stateFromVolumeView(this.props.volumeView)
-
-    private stateFromVolumeView(vv: VolumeView) {
-        return {
-            volumeView: vv,
-            label: vv.label,
-            volume: vv.volume,
-            active: vv.active,
-            volumeRepresentations: vv.volumeRepresentations
-        }
-    }
-
-    componentWillMount() {
-        this.setState(this.stateFromVolumeView(this.props.volumeView))
-    }
-
-    componentDidMount() {
-        const vv = this.props.volumeView
-
-        this.props.volumeView.updated.subscribe(() => this.setState({
-            volumeRepresentations: vv.volumeRepresentations
-        }))
-    }
-
-    componentWillReceiveProps(nextProps: VolumeViewComponentProps) {
-        if (nextProps.volumeView !== this.props.volumeView) {
-            this.setState(this.stateFromVolumeView(nextProps.volumeView))
-
-            nextProps.volumeView.updated.subscribe(() => this.setState({
-                volumeRepresentations: nextProps.volumeView.volumeRepresentations
-            }))
-        }
-    }
-
-    // async update(state: Partial<VolumeViewComponentState>) {
-    //     const vv = this.state.volumeView
-    //     this.setState(this.stateFromVolumeView(vv))
-    // }
-
-    render() {
-        const { volumeView, label, active, volumeRepresentations } = this.state
-
-        return <div>
-            <div>
-                <h2>{label}</h2>
-            </div>
-            <div>
-                <div>
-                    <h4>Active</h4>
-                    { Object.keys(active).map((k, i) => {
-                        return <div key={i}>
-                            <input
-                                type='checkbox'
-                                checked={active[k]}
-                                onChange={(e) => {
-                                    volumeView.setVolumeRepresentation(k, e.target.checked)
-                                }}
-                            /> {k}
-                        </div>
-                    } ) }
-                </div>
-                <div>
-                    <h3>Volume Representations</h3>
-                    { Object.keys(volumeRepresentations).map((k, i) => {
-                        if (active[k]) {
-                            return <div key={i}>
-                                <RepresentationComponent
-                                    repr={volumeRepresentations[k] as Representation<any>}
-                                    canvas3d={volumeView.viewer}
-                                    app={volumeView.app}
-                                />
-                            </div>
-                        } else {
-                            return ''
-                        }
-                    } ) }
-                </div>
-            </div>
-        </div>;
-    }
-}

+ 0 - 183
src/apps/canvas/examples.ts

@@ -1,183 +0,0 @@
-
-
-export interface Example {
-    label?: string
-    id: string
-    description: string
-}
-
-export const Examples: Example[] = [
-    {
-        id: '1jj2',
-        description: 'ribosome'
-    },
-    {
-        id: '1grm',
-        description: 'helix-like sheets'
-    },
-    {
-        id: '4umt',
-        description: 'ligand has bond with order 3'
-    },
-    {
-        id: '1crn',
-        description: 'small'
-    },
-    {
-        id: '1hrv',
-        description: 'viral assembly'
-    },
-    {
-        id: '1rb8',
-        description: 'virus'
-    },
-    {
-        id: '1blu',
-        description: 'metal coordination'
-    },
-    {
-        id: '3pqr',
-        description: 'inter unit bonds, two polymer chains, ligands, water, carbohydrates linked to protein'
-    },
-    {
-        id: '4v5a',
-        description: 'ribosome'
-    },
-    {
-        id: '6h7w',
-        description: 'retromer assembled on membrane'
-    },
-    {
-        id: '3j3q',
-        description: '...'
-    },
-    {
-        id: '5gob',
-        description: 'D-aminoacids'
-    },
-    {
-        id: '2np2',
-        description: 'dna'
-    },
-    {
-        id: '1d66',
-        description: 'dna'
-    },
-    {
-        id: '9dna',
-        description: 'A form dna'
-    },
-    {
-        id: '1bna',
-        description: 'B form dna'
-    },
-    {
-        id: '199d',
-        description: 'C form dna'
-    },
-    {
-        id: '4lb6',
-        description: 'Z form dna'
-    },
-    {
-        id: '1egk',
-        description: '4-way dna-rna junction'
-    },
-    {
-        id: '1y26',
-        description: 'rna'
-    },
-    {
-        id: '1xv6',
-        description: 'rna, modified nucleotides'
-    },
-    {
-        id: '3bbm',
-        description: 'rna with linker'
-    },
-    {
-        id: '1euq',
-        description: 't-rna'
-    },
-    {
-        id: '2e2i',
-        description: 'rna, dna, protein'
-    },
-    {
-        id: '1gfl',
-        description: 'GFP, flourophore has carbonyl oxygen removed'
-    },
-    {
-        id: '1sfi',
-        description: 'contains cyclic peptid'
-    },
-    {
-        id: '3sn6',
-        description: 'discontinuous chains'
-    },
-    {
-        id: '2zex',
-        description: 'contains carbohydrate polymer'
-    },
-    {
-        id: '3sgj',
-        description: 'contains carbohydrate polymer'
-    },
-    {
-        id: '3ina',
-        description: 'contains GlcN and IdoA'
-    },
-    {
-        id: '1umz',
-        description: 'contains Xyl (Xyloglucan)'
-    },
-    {
-        id: '1mfb',
-        description: 'contains Abe'
-    },
-    {
-        id: '2gdu',
-        description: 'contains sucrose'
-    },
-    {
-        id: '2fnc',
-        description: 'contains maltotriose'
-    },
-    {
-        id: '4zs9',
-        description: 'contains raffinose'
-    },
-    {
-        id: '2yft',
-        description: 'contains kestose'
-    },
-    {
-        id: '2b5t',
-        description: 'contains large carbohydrate polymer'
-    },
-    {
-        id: '1b5f',
-        description: 'contains carbohydrate with alternate locations'
-    },
-    {
-        id: '5u0q',
-        description: 'mixed dna/rna in same polymer'
-    },
-    {
-        id: '1xj9',
-        description: 'PNA (peptide nucleic acid)'
-    },
-    {
-        id: '5eme',
-        description: 'PNA (peptide nucleic acid) and RNA'
-    },
-    {
-        id: '2X3T',
-        description: 'temp'
-    },
-    {
-        label: 'ModelServer/1cbs/full',
-        id: 'http://localhost:1337/ModelServer/query?%7B%22id%22%3A%221cbs%22%2C%22name%22%3A%22full%22%7D',
-        description: '1cbs from model server'
-    }
-]

+ 0 - 33
src/apps/canvas/index.html

@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-    <head>
-        <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="app" style="width: 100%; height: 100%"></div>
-        <script type="text/javascript" src="./index.js"></script>
-    </body>
-</html>

+ 0 - 49
src/apps/canvas/index.ts

@@ -1,49 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react'
-import * as ReactDOM from 'react-dom'
-
-import './index.html'
-
-import { App } from './app';
-import { AppComponent } from './component/app';
-import { urlQueryParameter } from 'mol-util/url-query';
-
-const elm = document.getElementById('app') as HTMLElement
-if (!elm) throw new Error('Can not find element with id "app".')
-
-const app = new App()
-ReactDOM.render(React.createElement(AppComponent, { app }), elm);
-
-const assemblyId = urlQueryParameter('assembly')
-const pdbId = urlQueryParameter('pdb')
-if (pdbId) app.loadPdbIdOrMmcifUrl(pdbId, { assemblyId })
-
-// app.loadPdbIdOrMmcifUrl('http://localhost:8091/ngl/data/1crn.cif')
-
-// app.loadPdbIdOrMmcifUrl('3pqr')
-// app.loadCcp4Url('http://localhost:8091/ngl/data/3pqr-mode0.ccp4')
-
-app.loadPdbIdOrMmcifUrl('1lee')
-app.loadCcp4Url('http://localhost:8091/ngl/data/1lee.ccp4')
-
-// app.loadPdbIdOrMmcifUrl('6DRV')
-// app.loadCcp4Url('http://localhost:8091/ngl/data/betaGal.mrc')
-
-// app.loadPdbIdOrMmcifUrl('3pqr')
-// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/x-ray/3pqr/cell?space=fractional', true)
-
-// app.loadPdbIdOrMmcifUrl('5ire')
-// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/em/emd-8116/cell?space=cartesian&detail=6', true)
-
-// app.loadPdbIdOrMmcifUrl('5gag')
-// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/em/emd-8003/cell?detail=3', true)
-
-// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/1B5F-carb.cif')
-// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/2HYV-carb.cif')
-// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/2WMG-carb.cif')
-// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/5KDS-carb.cif')

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

@@ -1,369 +0,0 @@
-/**
- * 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 { getStructureFromModel } from './util';
-import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry';
-import { getAxesShape } from './assembly-symmetry';
-import { Canvas3D } from 'mol-canvas3d/canvas3d';
-// 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 { BehaviorSubject } from 'rxjs';
-import { App } from './app';
-import { StructureRepresentation } from 'mol-repr/structure/representation';
-import { ShapeRepresentation, ShapeParams } from 'mol-repr/shape/representation';
-
-export interface StructureView {
-    readonly app: App
-    readonly canvas3d: Canvas3D
-
-    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<ShapeParams>
-
-    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(app: App, canvas3d: Canvas3D, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> {
-    const active: { [k: string]: boolean } = {
-        'cartoon': true,
-        'ball-and-stick': true,
-        // point: false,
-        // surface: false,
-        // carbohydrate: false,
-        // spacefill: false,
-        // distanceRestraint: false,
-        // symmetryAxes: true,
-        // polymerSphere: false,
-    }
-
-    const structureRepresentations: { [k: string]: StructureRepresentation<any> } = {}
-
-    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 app.runTask(AssemblySymmetry.attachFromCifOrAPI(models[modelId]), 'Load symmetry annotation')
-            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 = 'deposited'
-        } else {
-            assemblyId = '-1'
-        }
-        await getStructure()
-        await setSymmetryFeature(newSymmetryFeatureId)
-    }
-
-    function getAssemblyIds() {
-        const assemblyIds: { id: string, label: string }[] = [
-            { id: 'deposited', label: 'deposited' }
-        ]
-        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 s = assemblySymmetry.getSymmetries(assemblyId)
-            if (s._rowCount) {
-                symmetryFeatureId = s.id.value(0)
-            } else {
-                symmetryFeatureId = -1
-            }
-        } else {
-            symmetryFeatureId = -1
-        }
-        await createSymmetryRepr()
-    }
-
-    function getSymmetryFeatureIds() {
-        const symmetryFeatureIds: { id: number, label: string }[] = []
-        if (assemblySymmetry) {
-            const symmetries = assemblySymmetry.getSymmetries(assemblyId)
-            for (let i = 0, il = symmetries._rowCount; i < il; ++i) {
-                const id = symmetries.id.value(i)
-                const kind = symmetries.kind.value(i)
-                const type = symmetries.type.value(i)
-                const stoichiometry = symmetries.stoichiometry.value(i)
-                const label = `${id}: ${kind} ${type} ${stoichiometry}`
-                symmetryFeatureIds.push({ id, label })
-            }
-        }
-        return symmetryFeatureIds
-    }
-
-    async function getStructure() {
-        if (model) structure = await app.runTask(getStructureFromModel(model, assemblyId), 'Build structure')
-        if (model && structure) {
-            label = `${model.label} - Assembly ${assemblyId}`
-        } else {
-            label = ''
-        }
-        await createStructureRepr()
-    }
-
-    async function createStructureRepr() {
-        if (structure) {
-            console.log('createStructureRepr')
-            for (const k in active) {
-                if (active[k]) {
-                    let repr: StructureRepresentation
-                    if (structureRepresentations[k]) {
-                        repr = structureRepresentations[k]
-                    } else {
-                        const provider = app.structureRepresentationRegistry.get(k)
-                        repr = provider.factory(provider.getParams)
-                        structureRepresentations[k] = repr
-                        canvas3d.add(repr)
-                    }
-                    await app.runTask(repr.createOrUpdate(app.reprCtx, {}, structure).run(
-                        progress => app.log(progress)
-                    ), 'Create/update representation')
-                } else {
-                    if (structureRepresentations[k]) {
-                        canvas3d.remove(structureRepresentations[k])
-                        structureRepresentations[k].destroy()
-                        delete structureRepresentations[k]
-                    }
-                }
-            }
-
-            canvas3d.camera.setState({ target: 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()
-        }
-
-        canvas3d.add(polymerSphere)
-
-        updated.next(null)
-        canvas3d.requestDraw(true)
-        console.log('stats', canvas3d.stats)
-    }
-
-    async function createSymmetryRepr() {
-        if (assemblySymmetry) {
-            const symmetries = assemblySymmetry.getSymmetries(assemblyId)
-            if (symmetries._rowCount) {
-                const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry)
-                if (axesShape) {
-                    // const colorTheme = getClusterColorTheme(symmetryFeatureId, assemblySymmetry)
-                    // await structureRepresentations['cartoon'].createOrUpdate({
-                    //     colorTheme: 'custom',
-                    //     colorFunction: colorTheme.color,
-                    //     colorGranularity: colorTheme.granularity,
-                    // }).run()
-                    await symmetryAxes.createOrUpdate(app.reprCtx, {}, axesShape).run()
-                    canvas3d.add(symmetryAxes)
-                } else {
-                    canvas3d.remove(symmetryAxes)
-                }
-            } else {
-                canvas3d.remove(symmetryAxes)
-            }
-        } else {
-            canvas3d.remove(symmetryAxes)
-        }
-        updated.next(null)
-        canvas3d.requestDraw(true)
-    }
-
-    await setModel(0, props.assemblyId, props.symmetryFeatureId)
-
-    return {
-        app,
-        canvas3d,
-
-        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) {
-                canvas3d.remove(structureRepresentations[k])
-                structureRepresentations[k].destroy()
-            }
-            canvas3d.remove(polymerSphere)
-            canvas3d.remove(symmetryAxes)
-            canvas3d.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)

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

@@ -1,78 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { readUrl, readFile, readUrlAsBuffer, readFileAsBuffer } from 'mol-util/read';
-import CIF, { CifBlock } from 'mol-io/reader/cif'
-import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure';
-import CCP4 from 'mol-io/reader/ccp4/parser'
-import { FileHandle } from 'mol-io/common/file-handle';
-import { Ccp4File } from 'mol-io/reader/ccp4/schema';
-import { volumeFromCcp4 } from 'mol-model/volume/formats/ccp4';
-import { parseDensityServerData } from 'mol-model/volume';
-// 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
-}
-
-export async function getCifFromUrl(url: string, binary = false) {
-    return getCifFromData(await readUrl(url, binary))
-}
-
-export async function getCifFromFile(file: File, binary = false) {
-    return getCifFromData(await readFile(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 === 'deposited') {
-        return Structure.ofModel(model)
-    } else if (assemblies.find(a => a.id === assembly)) {
-        return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run()
-    }
-}
-
-//
-
-export async function getCcp4FromUrl(url: string) {
-    return getCcp4FromData(await readUrlAsBuffer(url))
-}
-
-export async function getCcp4FromFile(file: File) {
-    return getCcp4FromData(await readFileAsBuffer(file))
-}
-
-export async function getCcp4FromData(data: Uint8Array) {
-    const file = FileHandle.fromBuffer(data)
-    const parsed = await CCP4(file).run()
-    if (parsed.isError) throw parsed
-    return parsed.result
-}
-
-export async function getVolumeFromCcp4(ccp4: Ccp4File) {
-    return await volumeFromCcp4(ccp4).run()
-}
-
-//
-
-export async function getVolumeFromVolcif(cif: CifBlock) {
-    return await parseDensityServerData(CIF.schema.densityServer(cif)).run()
-}

+ 0 - 97
src/apps/canvas/volume-view.ts

@@ -1,97 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Canvas3D } from 'mol-canvas3d/canvas3d';
-import { BehaviorSubject } from 'rxjs';
-import { App } from './app';
-import { VolumeData } from 'mol-model/volume';
-import { VolumeRepresentation } from 'mol-repr/volume/representation';
-import { IsosurfaceRepresentation } from 'mol-repr/volume/isosurface-mesh';
-import { DirectVolumeRepresentation } from 'mol-repr/volume/direct-volume';
-
-export interface VolumeView {
-    readonly app: App
-    readonly viewer: Canvas3D
-
-    readonly label: string
-    readonly volume: VolumeData
-
-    readonly active: { [k: string]: boolean }
-    readonly volumeRepresentations: { [k: string]: VolumeRepresentation<any> }
-    readonly updated: BehaviorSubject<null>
-
-    setVolumeRepresentation(name: string, value: boolean): void
-    destroy: () => void
-}
-
-interface VolumeViewProps {
-
-}
-
-export async function VolumeView(app: App, viewer: Canvas3D, volume: VolumeData, props: VolumeViewProps = {}): Promise<VolumeView> {
-    const active: { [k: string]: boolean } = {
-        isosurface: true,
-        directVolume: false,
-    }
-
-    const volumeRepresentations: { [k: string]: VolumeRepresentation<any> } = {
-        isosurface: IsosurfaceRepresentation(),
-        directVolume: DirectVolumeRepresentation(),
-    }
-
-    const updated: BehaviorSubject<null> = new BehaviorSubject<null>(null)
-
-    let label: string = 'Volume'
-
-    async function setVolumeRepresentation(k: string, value: boolean) {
-        active[k] = value
-        await createVolumeRepr()
-    }
-
-    async function createVolumeRepr() {
-        for (const k in volumeRepresentations) {
-            if (active[k]) {
-                await app.runTask(volumeRepresentations[k].createOrUpdate(app.reprCtx, {}, volume).run(
-                    progress => app.log(progress)
-                ), 'Create/update representation')
-                viewer.add(volumeRepresentations[k])
-            } else {
-                viewer.remove(volumeRepresentations[k])
-            }
-        }
-
-        // const center = Vec3.clone(volume.cell.size)
-        // Vec3.scale(center, center, 0.5)
-        // viewer.center(center)
-
-        updated.next(null)
-        viewer.requestDraw(true)
-        console.log('stats', viewer.stats)
-    }
-
-    await createVolumeRepr()
-
-    return {
-        app,
-        viewer,
-
-        get label() { return label },
-        volume,
-
-        active,
-        volumeRepresentations,
-        setVolumeRepresentation,
-        updated,
-
-        destroy: () => {
-            for (const k in volumeRepresentations) {
-                viewer.remove(volumeRepresentations[k])
-                volumeRepresentations[k].destroy()
-            }
-            viewer.requestDraw(true)
-        }
-    }
-}

+ 1 - 0
src/mol-canvas3d/camera.ts

@@ -97,6 +97,7 @@ class Camera implements Object3D {
         Vec3.setMagnitude(this.deltaDirection, this.state.direction, deltaDistance)
         if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection)
         Vec3.add(this.newPosition, this.state.position, this.deltaDirection)
+        
         this.setState({ target, position: this.newPosition })
     }
 

+ 13 - 18
src/mol-canvas3d/canvas3d.ts

@@ -35,11 +35,8 @@ export const Canvas3DParams = {
     cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
     backgroundColor: PD.Color(Color(0x000000)),
     // TODO: make this an interval?
-    clipNear: PD.Numeric(1, { min: 1, max: 100, step: 1 }),
-    clipFar: PD.Numeric(100, { min: 1, max: 100, step: 1 }),
-    // TODO: make this an interval?
-    fogNear: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
-    fogFar: PD.Numeric(100, { min: 1, max: 100, step: 1 }),
+    clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }),
+    fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }),
     pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
     showBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of render objects.' }),
     // debug: PD.Group({
@@ -168,13 +165,13 @@ namespace Canvas3D {
             const cDist = Vec3.distance(camera.state.position, camera.state.target)
             const bRadius = Math.max(10, scene.boundingSphere.radius)
 
-            const nearFactor = (50 - p.clipNear) / 50
-            const farFactor = -(50 - p.clipFar) / 50
+            const nearFactor = (50 - p.clip[0]) / 50
+            const farFactor = -(50 - p.clip[1]) / 50
             const near = cDist - (bRadius * nearFactor)
             const far = cDist + (bRadius * farFactor)
 
-            const fogNearFactor = (50 - p.fogNear) / 50
-            const fogFarFactor = -(50 - p.fogFar) / 50
+            const fogNearFactor = (50 - p.fog[0]) / 50
+            const fogFarFactor = -(50 - p.fog[1]) / 50
             const fogNear = cDist - (bRadius * fogNearFactor)
             const fogFar = cDist + (bRadius * fogFarFactor)
 
@@ -321,7 +318,9 @@ namespace Canvas3D {
 
             add: (repr: Representation.Any) => {
                 add(repr)
-                reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => add(repr)))
+                reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
+                    if (!repr.state.syncManually) add(repr)
+                }))
             },
             remove: (repr: Representation.Any) => {
                 const updatedSubscription = reprUpdatedSubscriptions.get(repr)
@@ -378,10 +377,8 @@ namespace Canvas3D {
                     renderer.setClearColor(props.backgroundColor)
                 }
 
-                if (props.clipNear !== undefined) p.clipNear = props.clipNear
-                if (props.clipFar !== undefined) p.clipFar = props.clipFar
-                if (props.fogNear !== undefined) p.fogNear = props.fogNear
-                if (props.fogFar !== undefined) p.fogFar = props.fogFar
+                if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]]
+                if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]]
 
                 if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) {
                     renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold)
@@ -396,10 +393,8 @@ namespace Canvas3D {
                 return {
                     cameraMode: camera.state.mode,
                     backgroundColor: renderer.props.clearColor,
-                    clipNear: p.clipNear,
-                    clipFar: p.clipFar,
-                    fogNear: p.fogNear,
-                    fogFar: p.fogFar,
+                    clip: p.clip,
+                    fog: p.fog,
                     pickingAlphaThreshold: renderer.props.pickingAlphaThreshold,
                     showBoundingSpheres: boundingSphereHelper.visible
                 }

+ 2 - 2
src/mol-canvas3d/helper/bounding-sphere-helper.ts

@@ -27,11 +27,11 @@ export class BoundingSphereHelper {
     update() {
         const builder = MeshBuilder.create(1024, 512, this.mesh)
         if (this.scene.boundingSphere.radius) {
-            addSphere(builder, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 3)
+            addSphere(builder, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 2)
         }
         this.scene.forEach(r => {
             if (r.boundingSphere.radius) {
-                addSphere(builder, r.boundingSphere.center, r.boundingSphere.radius, 3)
+                addSphere(builder, r.boundingSphere.center, r.boundingSphere.radius, 2)
             }
         })
         this.mesh = builder.getMesh()

+ 10 - 8
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -125,6 +125,16 @@ export namespace DirectVolume {
     }
 
     export function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
+        ValueCell.updateIfChanged(values.uIsoValue, props.isoValue)
+        ValueCell.updateIfChanged(values.uAlpha, props.alpha)
+        ValueCell.updateIfChanged(values.dUseFog, props.useFog)
+        ValueCell.updateIfChanged(values.dRenderMode, props.renderMode)
+
+        const controlPoints = getControlPointsFromString(props.controlPoints)
+        createTransferFunctionTexture(controlPoints, values.tTransferTex)
+    }
+
+    export function updateBoundingSphere(values: DirectVolumeValues, directVolume: DirectVolume) {
         const vertices = new Float32Array(values.aPosition.ref.value)
         transformPositionArray(values.uTransform.ref.value, vertices, 0, vertices.length / 3)
         const boundingSphere = calculateBoundingSphere(
@@ -134,13 +144,5 @@ export namespace DirectVolume {
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
-
-        ValueCell.updateIfChanged(values.uIsoValue, props.isoValue)
-        ValueCell.updateIfChanged(values.uAlpha, props.alpha)
-        ValueCell.updateIfChanged(values.dUseFog, props.useFog)
-        ValueCell.updateIfChanged(values.dRenderMode, props.renderMode)
-
-        const controlPoints = getControlPointsFromVec2Array(props.controlPoints)
-        createTransferFunctionTexture(controlPoints, values.tTransferTex)
     }
 }

+ 0 - 5
src/mol-geo/geometry/geometry.ts

@@ -15,8 +15,6 @@ import { SizeType } from './size-data';
 import { Lines } from './lines/lines';
 import { ParamDefinition as PD } from 'mol-util/param-definition'
 import { DirectVolume } from './direct-volume/direct-volume';
-import { BuiltInSizeThemeOptions, getBuiltInSizeThemeParams } from 'mol-theme/size';
-import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color';
 import { Color } from 'mol-util/color';
 import { Vec3 } from 'mol-math/linear-algebra';
 
@@ -67,9 +65,6 @@ export namespace Geometry {
         selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
 
         quality: PD.Select<VisualQuality>('auto', VisualQualityOptions),
-
-        colorTheme: PD.Mapped('uniform', BuiltInColorThemeOptions, getBuiltInColorThemeParams),
-        sizeTheme: PD.Mapped('uniform', BuiltInSizeThemeOptions, getBuiltInSizeThemeParams),
     }
     export type Params = typeof Params
 

+ 7 - 5
src/mol-geo/geometry/lines/lines.ts

@@ -138,21 +138,23 @@ export namespace Lines {
     }
 
     export function updateValues(values: LinesValues, props: PD.Values<Params>) {
+        Geometry.updateValues(values, props)
+        ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation)
+    }
+
+    export function updateBoundingSphere(values: LinesValues, lines: Lines) {
         const boundingSphere = Sphere3D.addSphere(
             calculateBoundingSphere(
-                values.aStart.ref.value, Math.floor(values.aStart.ref.value.length / 3),
+                values.aStart.ref.value, lines.lineCount,
                 values.aTransform.ref.value, values.instanceCount.ref.value
             ),
             calculateBoundingSphere(
-                values.aEnd.ref.value, Math.floor(values.aEnd.ref.value.length / 3),
+                values.aEnd.ref.value, lines.lineCount,
                 values.aTransform.ref.value, values.instanceCount.ref.value
             ),
         )
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
-
-        Geometry.updateValues(values, props)
-        ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation)
     }
 }

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

@@ -15,6 +15,8 @@ const tV = Vec3.zero()
 
 const horizontalVector = Vec3.zero()
 const verticalVector = Vec3.zero()
+const verticalRightVector = Vec3.zero()
+const verticalLeftVector = Vec3.zero()
 const normalOffset = Vec3.zero()
 const positionVector = Vec3.zero()
 const normalVector = Vec3.zero()
@@ -25,6 +27,41 @@ const p2 = Vec3.zero()
 const p3 = Vec3.zero()
 const p4 = Vec3.zero()
 
+function addCap(offset: number, builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
+    const { vertices, normals, indices } = builder.state
+    const vertexCount = vertices.elementCount
+
+    Vec3.fromArray(verticalLeftVector, normalVectors, offset)
+    Vec3.scale(verticalLeftVector, verticalLeftVector, leftHeight)
+
+    Vec3.fromArray(verticalRightVector, normalVectors, offset)
+    Vec3.scale(verticalRightVector, verticalRightVector, rightHeight)
+
+    Vec3.fromArray(horizontalVector, binormalVectors, offset)
+    Vec3.scale(horizontalVector, horizontalVector, width)
+
+    Vec3.fromArray(positionVector, controlPoints, offset)
+
+    Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalRightVector)
+    Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalLeftVector)
+    Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalLeftVector)
+    Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalRightVector)
+
+    ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+    ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+    ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+    ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+
+    Vec3.cross(normalVector, horizontalVector, verticalLeftVector)
+
+    for (let i = 0; i < 4; ++i) {
+        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+    }
+    ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount)
+    ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2)
+}
+
+/** set arrowHeight = 0 for no arrow */
 export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) {
     const { currentGroup, vertices, normals, indices, groups } = builder.state
 
@@ -112,67 +149,19 @@ export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>,
     }
 
     if (startCap) {
-        const offset = 0
-        vertexCount = vertices.elementCount
-
-        Vec3.fromArray(verticalVector, normalVectors, offset)
-        Vec3.scale(verticalVector, verticalVector, arrowHeight === 0 ? height : arrowHeight);
-
-        Vec3.fromArray(horizontalVector, binormalVectors, offset)
-        Vec3.scale(horizontalVector, horizontalVector, width);
-
-        Vec3.fromArray(positionVector, controlPoints, offset)
-
-        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
-        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
-        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
-        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
-
-        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
-        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
-        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
-        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
-
-        Vec3.cross(normalVector, horizontalVector, verticalVector)
-
-        for (let i = 0; i < 4; ++i) {
-            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
-        }
-        ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-        ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+        const h = arrowHeight === 0 ? height : arrowHeight
+        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, h, h)
+    } else if (arrowHeight > 0) {
+        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
+        addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height)
     }
 
     if (endCap && arrowHeight === 0) {
-        const offset = linearSegments * 3
-        vertexCount = vertices.elementCount
-
-        Vec3.fromArray(verticalVector, normalVectors, offset)
-        Vec3.scale(verticalVector, verticalVector, height);
-
-        Vec3.fromArray(horizontalVector, binormalVectors, offset)
-        Vec3.scale(horizontalVector, horizontalVector, width);
-
-        Vec3.fromArray(positionVector, controlPoints, offset)
-
-        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
-        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
-        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
-        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
-
-        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
-        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
-        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
-        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
-
-        Vec3.cross(normalVector, horizontalVector, verticalVector)
-
-        for (let i = 0; i < 4; ++i) {
-            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
-        }
-        ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
-        ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+        addCap(linearSegments * 3, builder, controlPoints, normalVectors, binormalVectors, width, height, height)
     }
 
-    const addedVertexCount = (linearSegments + 1) * 8 + (startCap ? 4 : 0) + (endCap && arrowHeight === 0 ? 4 : 0)
+    const addedVertexCount = (linearSegments + 1) * 8 + 
+        (startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) + 
+        (endCap && arrowHeight === 0 ? 4 : 0)
     for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup)
 }

+ 8 - 6
src/mol-geo/geometry/mesh/mesh.ts

@@ -409,18 +409,20 @@ export namespace Mesh {
     }
 
     export function updateValues(values: MeshValues, props: PD.Values<Params>) {
+        Geometry.updateValues(values, props)
+        ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided)
+        ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded)
+        ValueCell.updateIfChanged(values.dFlipSided, props.flipSided)
+    }
+
+    export function updateBoundingSphere(values: MeshValues, mesh: Mesh) {
         const boundingSphere = calculateBoundingSphere(
-            values.aPosition.ref.value, Math.floor(values.aPosition.ref.value.length / 3),
+            values.aPosition.ref.value, mesh.vertexCount,
             values.aTransform.ref.value, values.instanceCount.ref.value
         )
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
-
-        Geometry.updateValues(values, props)
-        ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided)
-        ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded)
-        ValueCell.updateIfChanged(values.dFlipSided, props.flipSided)
     }
 }
 

+ 8 - 6
src/mol-geo/geometry/points/points.ts

@@ -93,17 +93,19 @@ export namespace Points {
     }
 
     export function updateValues(values: PointsValues, props: PD.Values<Params>) {
+        Geometry.updateValues(values, props)
+        ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation)
+        ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle)
+        ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach)
+    }
+
+    export function updateBoundingSphere(values: PointsValues, points: Points) {
         const boundingSphere = calculateBoundingSphere(
-            values.aPosition.ref.value, Math.floor(values.aPosition.ref.value.length / 3),
+            values.aPosition.ref.value, points.pointCount,
             values.aTransform.ref.value, values.instanceCount.ref.value
         )
         if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
             ValueCell.update(values.boundingSphere, boundingSphere)
         }
-
-        Geometry.updateValues(values, props)
-        ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation)
-        ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle)
-        ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach)
     }
 }

+ 12 - 5
src/mol-gl/scene.ts

@@ -11,7 +11,9 @@ import { RenderObject, createRenderable } from './render-object';
 import { Object3D } from './object3d';
 import { Sphere3D } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { BoundaryHelper } from 'mol-math/geometry/boundary-helper';
 
+const boundaryHelper = new BoundaryHelper();
 function calculateBoundingSphere(renderableMap: Map<RenderObject, Renderable<RenderableValues & BaseValues>>, boundingSphere: Sphere3D): Sphere3D {
     // let count = 0
     // const center = Vec3.set(boundingSphere.center, 0, 0, 0)
@@ -33,15 +35,20 @@ function calculateBoundingSphere(renderableMap: Map<RenderObject, Renderable<Ren
     // })
     // boundingSphere.radius = radius
 
-    const spheres: Sphere3D[] = [];
+    boundaryHelper.reset(0.1);
+
+    renderableMap.forEach(r => {
+        if (!r.state.visible || !r.boundingSphere.radius) return;
+        boundaryHelper.boundaryStep(r.boundingSphere.center, r.boundingSphere.radius);
+    });
+    boundaryHelper.finishBoundaryStep();
     renderableMap.forEach(r => {
         if (!r.state.visible || !r.boundingSphere.radius) return;
-        spheres.push(r.boundingSphere)
+        boundaryHelper.extendStep(r.boundingSphere.center, r.boundingSphere.radius);
     });
-    const bs = Sphere3D.getBoundingSphereFromSpheres(spheres, 0.1);
 
-    Vec3.copy(boundingSphere.center, bs.center);
-    boundingSphere.radius = bs.radius;
+    Vec3.copy(boundingSphere.center, boundaryHelper.center);
+    boundingSphere.radius = boundaryHelper.radius;
 
     return boundingSphere;
 }

+ 1 - 1
src/mol-gl/shader/chunks/apply-marker-color.glsl

@@ -1,6 +1,6 @@
 // only mark elements with an alpha above the picking threshold
 if (uAlpha >= uPickingAlphaThreshold) {
-    float marker = vMarker * 255.0;
+    float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
     if (marker > 0.1) {
         if (intMod(marker, 2.0) > 0.1) {
             gl_FragColor.rgb = mix(uHighlightColor, gl_FragColor.rgb, 0.3);

+ 14 - 5
src/mol-gl/webgl/render-item.ts

@@ -216,11 +216,20 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
             }
 
             if (valueChanges.attributes || valueChanges.defines || valueChanges.elements) {
-                // console.log('program/defines or buffers changed, rebuild vaos')
-                Object.keys(RenderVariantDefines).forEach(k => {
-                    deleteVertexArray(ctx, vertexArrays[k])
-                    vertexArrays[k] = createVertexArray(ctx, programs[k].value, attributeBuffers, elementsBuffer)
-                })
+                // console.log('program/defines or buffers changed, update vaos')
+                const { vertexArrayObject } = ctx.extensions
+                if (vertexArrayObject) {
+                    Object.keys(RenderVariantDefines).forEach(k => {
+                        vertexArrayObject.bindVertexArray(vertexArrays[k])
+                        if (elementsBuffer && (valueChanges.defines || valueChanges.elements)) {
+                            elementsBuffer.bind()
+                        }
+                        if (valueChanges.attributes || valueChanges.defines) {
+                            programs[k].value.bindAttributes(attributeBuffers)
+                        }
+                        vertexArrayObject.bindVertexArray(null)
+                    })
+                }
             }
 
             valueChanges.textures = false

+ 11 - 1
src/mol-gl/webgl/vertex-array.ts

@@ -17,11 +17,21 @@ export function createVertexArray(ctx: WebGLContext, program: Program, attribute
         if (elementsBuffer) elementsBuffer.bind()
         program.bindAttributes(attributeBuffers)
         ctx.vaoCount += 1
-        vertexArrayObject.bindVertexArray(null!)
+        vertexArrayObject.bindVertexArray(null)
     }
     return vertexArray
 }
 
+export function updateVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) {
+    const { vertexArrayObject } = ctx.extensions
+    if (vertexArrayObject && vertexArray) {
+        vertexArrayObject.bindVertexArray(vertexArray)
+        if (elementsBuffer) elementsBuffer.bind()
+        program.bindAttributes(attributeBuffers)
+        vertexArrayObject.bindVertexArray(null)
+    }
+}
+
 export function deleteVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null) {
     const { vertexArrayObject } = ctx.extensions
     if (vertexArrayObject && vertexArray) {

+ 124 - 0
src/mol-math/geometry/boundary-helper.ts

@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra/3d';
+import { Box3D } from './primitives/box3d';
+import { Sphere3D } from './primitives/sphere3d';
+
+/**
+ * Usage:
+ *
+ * 1. .reset(tolerance); tolerance plays part in the "extend" step
+ * 2. for each point/sphere call boundaryStep()
+ * 3. .finishBoundaryStep
+ * 4. for each point/sphere call extendStep
+ * 5. use .center/.radius or call getSphere/getBox
+ */
+export class BoundaryHelper {
+    private count = 0;
+    private extremes = [Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero()];
+    private u = Vec3.zero();
+    private v = Vec3.zero();
+
+    tolerance = 0;
+    center: Vec3 = Vec3.zero();
+    radius = 0;
+
+    reset(tolerance: number) {
+        Vec3.set(this.center, 0, 0, 0);
+        for (let i = 0; i < 6; i++) {
+            const e = i % 2 === 0 ? Number.MAX_VALUE : -Number.MAX_VALUE;
+            this.extremes[i] = Vec3.create(e, e, e);
+        }
+        this.radius = 0;
+        this.count = 0;
+        this.tolerance = tolerance;
+    }
+
+    boundaryStep(p: Vec3, r: number) {
+        updateExtremeMin(0, this.extremes[0], p, r);
+        updateExtremeMax(0, this.extremes[1], p, r);
+
+        updateExtremeMin(1, this.extremes[2], p, r);
+        updateExtremeMax(1, this.extremes[3], p, r);
+
+        updateExtremeMin(2, this.extremes[4], p, r);
+        updateExtremeMax(2, this.extremes[5], p, r);
+        this.count++;
+    }
+
+    finishBoundaryStep() {
+        if (this.count === 0) return;
+
+        let maxSpan = 0, mI = 0, mJ = 0;
+
+        for (let i = 0; i < 5; i++) {
+            for (let j = i + 1; j < 6; j++) {
+                const d = Vec3.squaredDistance(this.extremes[i], this.extremes[j]);
+                if (d > maxSpan) {
+                    maxSpan = d;
+                    mI = i;
+                    mJ = j;
+                }
+            }
+        }
+
+        Vec3.add(this.center, this.extremes[mI], this.extremes[mJ]);
+        Vec3.scale(this.center, this.center, 0.5);
+        this.radius = Vec3.distance(this.center, this.extremes[mI]);
+    }
+
+    extendStep(p: Vec3, r: number) {
+        const d = Vec3.distance(p, this.center);
+        if ((1 + this.tolerance) * this.radius >= r + d) return;
+
+        Vec3.sub(this.u, p, this.center);
+        Vec3.normalize(this.u, this.u);
+
+        Vec3.scale(this.v, this.u, -this.radius);
+        Vec3.add(this.v, this.v, this.center);
+        Vec3.scale(this.u, this.u, r + d);
+        Vec3.add(this.u, this.u, this.center);
+
+        Vec3.add(this.center, this.u, this.v);
+        Vec3.scale(this.center, this.center, 0.5);
+        this.radius = 0.5 * (r + d + this.radius);
+    }
+
+    getBox(): Box3D {
+        Vec3.copy(this.u, this.extremes[0]);
+        Vec3.copy(this.v, this.extremes[0]);
+
+        for (let i = 1; i < 6; i++) {
+            Vec3.min(this.u, this.u, this.extremes[i]);
+            Vec3.max(this.v, this.v, this.extremes[i]);
+        }
+
+        return { min: Vec3.clone(this.u), max: Vec3.clone(this.v) };
+    }
+
+    getSphere(): Sphere3D {
+        return { center: Vec3.clone(this.center), radius: this.radius };
+    }
+
+    constructor() {
+        this.reset(0);
+    }
+}
+
+function updateExtremeMin(d: number, e: Vec3, center: Vec3, r: number) {
+    if (center[d] - r < e[d]) {
+        Vec3.copy(e, center);
+        e[d] -= r;
+    }
+}
+
+function updateExtremeMax(d: number, e: Vec3, center: Vec3, r: number) {
+    if (center[d] + r > e[d]) {
+        Vec3.copy(e, center);
+        e[d] += r;
+    }
+}

+ 22 - 2
src/mol-math/geometry/lookup3d/grid.ts

@@ -11,6 +11,7 @@ import { Sphere3D } from '../primitives/sphere3d';
 import { PositionData } from '../common';
 import { Vec3 } from '../../linear-algebra';
 import { OrderedSet } from 'mol-data/int';
+import { BoundaryHelper } from '../boundary-helper';
 
 interface GridLookup3D<T = number> extends Lookup3D<T> {
     readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
@@ -163,11 +164,30 @@ function _build(state: BuildState): Grid3D {
     }
 }
 
+const boundaryHelper = new BoundaryHelper();
+function getBoundary(data: PositionData) {
+    const { x, y, z, radius, indices } = data;
+    const p = Vec3.zero();
+    boundaryHelper.reset(0);
+    for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
+        const i = OrderedSet.getAt(indices, t);
+        Vec3.set(p, x[i], y[i], z[i]);
+        boundaryHelper.boundaryStep(p, (radius && radius[i]) || 0);
+    }
+    boundaryHelper.finishBoundaryStep();
+    for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
+        const i = OrderedSet.getAt(indices, t);
+        Vec3.set(p, x[i], y[i], z[i]);
+        boundaryHelper.extendStep(p, (radius && radius[i]) || 0);
+    }
+
+    return { boundingBox: boundaryHelper.getBox(), boundingSphere: boundaryHelper.getSphere() };
+}
+
 function build(data: PositionData, cellSize?: Vec3) {
-    const boundingBox = Box3D.computeBounding(data);
+    const { boundingBox, boundingSphere } = getBoundary(data);
     // need to expand the grid bounds to avoid rounding errors
     const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
-    const boundingSphere = Sphere3D.computeBounding(data);
     const { indices } = data;
 
     const S = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min);

+ 0 - 77
src/mol-math/geometry/primitives/sphere3d.ts

@@ -96,83 +96,6 @@ namespace Sphere3D {
         return (Math.abs(ar - br) <= EPSILON.Value * Math.max(1.0, Math.abs(ar), Math.abs(br)) &&
                 Vec3.equals(a.center, b.center));
     }
-
-    function updateExtremeMin(d: number, e: Vec3, center: Vec3, r: number) {
-        if (center[d] - r < e[d]) {
-            Vec3.copy(e, center);
-            e[d] -= r;
-        }
-    }
-
-    function updateExtremeMax(d: number, e: Vec3, center: Vec3, r: number) {
-        if (center[d] + r > e[d]) {
-            Vec3.copy(e, center);
-            e[d] += r;
-        }
-    }
-
-    export function getBoundingSphereFromSpheres(spheres: Sphere3D[], tolerance: number): Sphere3D {
-        if (spheres.length === 0) {
-            return { center: Vec3.zero(), radius: 0.1 };
-        }
-
-        const extremes: Vec3[] = [];
-        for (let i = 0; i < 6; i++) {
-            const e = i % 2 === 0 ? Number.MAX_VALUE : -Number.MAX_VALUE;
-            extremes[i] = Vec3.create(e, e, e);
-        }
-        const u = Vec3.zero(), v = Vec3.zero();
-
-        let m = 0;
-        for (const s of spheres) {
-            updateExtremeMin(0, extremes[0], s.center, s.radius);
-            updateExtremeMax(0, extremes[1], s.center, s.radius);
-
-            updateExtremeMin(1, extremes[2], s.center, s.radius);
-            updateExtremeMax(1, extremes[3], s.center, s.radius);
-
-            updateExtremeMin(2, extremes[4], s.center, s.radius);
-            updateExtremeMax(2, extremes[5], s.center, s.radius);
-            if (s.radius > m) m = s.radius;
-        }
-
-        let maxSpan = 0, mI = 0, mJ = 0;
-
-        for (let i = 0; i < 5; i++) {
-            for (let j = i + 1; j < 6; j++) {
-                const d = Vec3.squaredDistance(extremes[i], extremes[j]);
-                if (d > maxSpan) {
-                    maxSpan = d;
-                    mI = i;
-                    mJ = j;
-                }
-            }
-        }
-
-        const center = Vec3.zero();
-        Vec3.add(center, extremes[mI], extremes[mJ]);
-        Vec3.scale(center, center, 0.5);
-        let radius = Vec3.distance(center, extremes[mI]);
-
-        for (const s of spheres) {
-            const d = Vec3.distance(s.center, center);
-            if ((1 + tolerance) * radius >= s.radius + d) continue;
-
-            Vec3.sub(u, s.center, center);
-            Vec3.normalize(u, u);
-
-            Vec3.scale(v, u, -radius);
-            Vec3.add(v, v, center);
-            Vec3.scale(u, u, s.radius + d);
-            Vec3.add(u, u, center);
-
-            Vec3.add(center, u, v);
-            Vec3.scale(center, center, 0.5);
-            radius = Vec3.distance(center, u);
-        }
-
-        return { center, radius };
-    }
 }
 
 export { Sphere3D }

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

@@ -58,27 +58,27 @@ namespace SymmetryOperator {
         return create(second.name, matrix, second.hkl);
     }
 
-    export interface CoordinateMapper { (index: number, slot: Vec3): Vec3 }
-    export interface ArrayMapping {
+    export interface CoordinateMapper<T extends number> { (index: T, slot: Vec3): Vec3 }
+    export interface ArrayMapping<T extends number> {
         readonly operator: SymmetryOperator,
-        readonly invariantPosition: CoordinateMapper,
-        readonly position: CoordinateMapper,
-        x(index: number): number,
-        y(index: number): number,
-        z(index: number): number,
-        r(index: number): number
+        readonly invariantPosition: CoordinateMapper<T>,
+        readonly position: CoordinateMapper<T>,
+        x(index: T): number,
+        y(index: T): number,
+        z(index: T): number,
+        r(index: T): number
     }
 
     export interface Coordinates { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }
 
-    export function createMapping(operator: SymmetryOperator, coords: Coordinates, radius: ((index: number) => number) | undefined): ArrayMapping {
+    export function createMapping<T extends number>(operator: SymmetryOperator, coords: Coordinates, radius: ((index: T) => number) | undefined): ArrayMapping<T> {
         const invariantPosition = SymmetryOperator.createCoordinateMapper(SymmetryOperator.Default, coords);
         const position = operator.isIdentity ? invariantPosition : SymmetryOperator.createCoordinateMapper(operator, coords);
         const { x, y, z } = createProjections(operator, coords);
         return { operator, invariantPosition, position, x, y, z, r: radius ? radius : _zeroRadius };
     }
 
-    export function createCoordinateMapper(t: SymmetryOperator, coords: Coordinates): CoordinateMapper {
+    export function createCoordinateMapper<T extends number>(t: SymmetryOperator, coords: Coordinates): CoordinateMapper<T> {
         if (t.isIdentity) return identityPosition(coords);
         return generalPosition(t, coords);
     }
@@ -145,7 +145,7 @@ function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: Symm
     }
 }
 
-function identityPosition({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper {
+function identityPosition<T extends number>({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper<T> {
     return (i, s) => {
         s[0] = x[i];
         s[1] = y[i];
@@ -154,10 +154,10 @@ function identityPosition({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOp
     }
 }
 
-function generalPosition({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
+function generalPosition<T extends number>({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) {
     if (isW1(m)) {
         // this should always be the case.
-        return (i: number, r: Vec3): Vec3 => {
+        return (i: T, r: Vec3): Vec3 => {
             const x = xs[i], y = ys[i], z = zs[i];
             r[0] = m[0] * x + m[4] * y + m[8] * z + m[12];
             r[1] = m[1] * x + m[5] * y + m[9] * z + m[13];
@@ -165,7 +165,7 @@ function generalPosition({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs
             return r;
         }
     }
-    return (i: number, r: Vec3): Vec3 => {
+    return (i: T, r: Vec3): Vec3 => {
         r[0] = xs[i];
         r[1] = ys[i];
         r[2] = zs[i];

+ 28 - 0
src/mol-model-props/pdbe/structure-quality-report.ts

@@ -15,6 +15,7 @@ import { CustomPropSymbol } from 'mol-script/language/symbol';
 import Type from 'mol-script/language/type';
 import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler';
 import { PropertyWrapper } from '../common/wrapper';
+import { Task } from 'mol-task';
 
 export namespace StructureQualityReport {
     export type IssueMap = IndexedCustomProperty.Residue<string[]>
@@ -82,6 +83,33 @@ export namespace StructureQualityReport {
         }
     }
 
+    export function createAttachTask(mapUrl: (model: Model) => string, fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) {
+        return (model: Model) => Task.create('PDBe Structure Quality Report', async ctx => {
+            if (get(model)) return true;
+
+            let issueMap: IssueMap | undefined;
+            let info;
+            // TODO: return from CIF support once the data is recomputed
+            //  = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model);
+            // if (info) {
+            //     const data = getCifData(model);
+            //     issueMap = createIssueMapFromCif(model, data.residues, data.groups);
+            // } else
+            {
+                const url = mapUrl(model);
+                const dataStr = await fetch(url, 'string').runInContext(ctx) as string;
+                const data = JSON.parse(dataStr)[model.label.toLowerCase()];
+                if (!data) return false;
+                info = PropertyWrapper.createInfo();
+                issueMap = createIssueMapFromJson(model, data);
+            }
+
+            model.customProperties.add(Descriptor);
+            set(model, { info, data: issueMap });
+            return false;
+        });
+    }
+
     export async function attachFromCifOrApi(model: Model, params: {
         // optional JSON source
         PDBe_apiSourceJson?: (model: Model) => Promise<any>

+ 54 - 0
src/mol-model-props/pdbe/themes/structure-quality-report.ts

@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report';
+import { Location } from 'mol-model/location';
+import { StructureElement } from 'mol-model/structure';
+import { ColorTheme, LocationColor } from 'mol-theme/color';
+import { ThemeDataContext } from 'mol-theme/theme';
+import { Color } from 'mol-util/color';
+import { TableLegend } from 'mol-util/color/tables';
+
+const ValidationColors = [
+    Color.fromRgb(170, 170, 170), // not applicable
+    Color.fromRgb(0, 255, 0), // 0 issues
+    Color.fromRgb(255, 255, 0), // 1
+    Color.fromRgb(255, 128, 0), // 2
+    Color.fromRgb(255, 0, 0), // 3 or more
+]
+
+const ValidationColorTable: [string, Color][] = [
+    ['No Issues', ValidationColors[1]],
+    ['One Issue', ValidationColors[2]],
+    ['Two Issues', ValidationColors[3]],
+    ['Three Or More Issues', ValidationColors[4]],
+    ['Not Applicable', ValidationColors[9]]
+]
+
+export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> {
+    let color: LocationColor
+
+    if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) {
+        const getIssues = StructureQualityReport.getIssues;
+        color = (location: Location) => {
+            if (StructureElement.isLocation(location)) {
+                return ValidationColors[Math.min(3, getIssues(location).length) + 1];
+            }
+            return ValidationColors[0];
+        }
+    } else {
+        color = () => ValidationColors[0];
+    }
+
+    return {
+        factory: StructureQualityReportColorTheme,
+        granularity: 'group',
+        color: color,
+        props: props,
+        description: 'Assigns residue colors according to the number of issues in the PDBe Validation Report.',
+        legend: TableLegend(ValidationColorTable)
+    }
+}

+ 8 - 2
src/mol-model/loci.ts

@@ -11,6 +11,7 @@ import { Sphere3D } from 'mol-math/geometry';
 import { CentroidHelper } from 'mol-math/geometry/centroid-helper';
 import { Vec3 } from 'mol-math/linear-algebra';
 import { OrderedSet } from 'mol-data/int';
+import { Structure } from './structure/structure';
 
 /** A Loci that includes every loci */
 export const EveryLoci = { kind: 'every-loci' as 'every-loci' }
@@ -29,6 +30,9 @@ export function isEmptyLoci(x: any): x is EmptyLoci {
 export function areLociEqual(lociA: Loci, lociB: Loci) {
     if (isEveryLoci(lociA) && isEveryLoci(lociB)) return true
     if (isEmptyLoci(lociA) && isEmptyLoci(lociB)) return true
+    if (Structure.isLoci(lociA) && Structure.isLoci(lociB)) {
+        return Structure.areLociEqual(lociA, lociB)
+    }
     if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) {
         return StructureElement.areLociEqual(lociA, lociB)
     }
@@ -44,7 +48,7 @@ export function areLociEqual(lociA: Loci, lociB: Loci) {
 
 export { Loci }
 
-type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci
+type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci
 
 namespace Loci {
 
@@ -54,7 +58,9 @@ namespace Loci {
         if (loci.kind === 'every-loci' || loci.kind === 'empty-loci') return void 0;
 
         sphereHelper.reset();
-        if (loci.kind === 'element-loci') {
+        if (loci.kind === 'structure-loci') {
+            return Sphere3D.clone(loci.structure.boundary.sphere)
+        } else if (loci.kind === 'element-loci') {
             for (const e of loci.elements) {
                 const { indices } = e;
                 const pos = e.unit.conformation.position;

+ 12 - 24
src/mol-model/structure/model/formats/mmcif.ts

@@ -24,10 +24,10 @@ import { getSequence } from './mmcif/sequence';
 import { sortAtomSite } from './mmcif/sort';
 import { StructConn } from './mmcif/bonds/struct_conn';
 import { ChemicalComponent, ChemicalComponentMap } from '../properties/chemical-component';
-import { ComponentType, getMoleculeType } from '../types';
+import { ComponentType, getMoleculeType, MoleculeType } from '../types';
 
 import mmCIF_Format = Format.mmCIF
-import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap } from 'mol-model/structure/structure/carbohydrates/constants';
+import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from 'mol-model/structure/structure/carbohydrates/constants';
 
 type AtomSite = mmCIF_Database['atom_site']
 
@@ -88,24 +88,6 @@ function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['m
     return { parentId, details };
 }
 
-function getAsymIdSerialMap(format: mmCIF_Format): ReadonlyMap<string, number> {
-    const data = format.data.struct_asym;
-    const map = new Map<string, number>();
-    let serial = 0
-
-    const id = data.id
-    const count = data._rowCount
-    for (let i = 0; i < count; ++i) {
-        const _id = id.value(i)
-        if (!map.has(_id)) {
-            map.set(_id, serial)
-            serial += 1
-        }
-    }
-
-    return map;
-}
-
 function getChemicalComponentMap(format: mmCIF_Format): ChemicalComponentMap {
     const map = new Map<string, ChemicalComponent>();
     const { id, type, name, pdbx_synonyms, formula, formula_weight } = format.data.chem_comp
@@ -142,15 +124,22 @@ function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap
                 }
             }
         }
-        return map
     } else {
-        return SaccharideCompIdMap
+        SaccharideCompIdMap.forEach((v, k) => map.set(k, v))
+        const { id, type  } = format.data.chem_comp
+        for (let i = 0, il = id.rowCount; i < il; ++i) {
+            const _id = id.value(i)
+            const _type = type.value(i)
+            if (!map.has(_id) && getMoleculeType(_type, _id) === MoleculeType.saccharide) {
+                map.set(_id, UnknownSaccharideComponent)
+            }
+        }
     }
+    return map
 }
 
 export interface FormatData {
     modifiedResidues: Model['properties']['modifiedResidues']
-    asymIdSerialMap: Model['properties']['asymIdSerialMap']
     chemicalComponentMap: Model['properties']['chemicalComponentMap']
     saccharideComponentMap: Model['properties']['saccharideComponentMap']
 }
@@ -158,7 +147,6 @@ export interface FormatData {
 function getFormatData(format: mmCIF_Format): FormatData {
     return {
         modifiedResidues: getModifiedResidueNameMap(format),
-        asymIdSerialMap: getAsymIdSerialMap(format),
         chemicalComponentMap: getChemicalComponentMap(format),
         saccharideComponentMap: getSaccharideComponentMap(format)
     }

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

@@ -47,8 +47,6 @@ export interface Model extends Readonly<{
             parentId: ReadonlyMap<string, string>,
             details: ReadonlyMap<string, string>
         }>,
-        /** maps asym id to unique serial number */
-        readonly asymIdSerialMap: ReadonlyMap<string, number>
         /** maps residue name to `ChemicalComponent` data */
         readonly chemicalComponentMap: ChemicalComponentMap
         /** maps residue name to `SaccharideComponent` data */

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

@@ -11,13 +11,14 @@ import Structure from '../../structure/structure';
 import { StructureQuery } from '../query';
 import { StructureSelection } from '../selection';
 
-export function sequence(): StructureQuery {
+export function atomicSequence(): StructureQuery {
     return ctx => {
         const { inputStructure } = ctx;
         const l = StructureElement.create();
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
             l.unit = unit;
             const elements = unit.elements;
             l.element = elements[0];
@@ -45,6 +46,8 @@ export function water(): StructureQuery {
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
             l.unit = unit;
             const elements = unit.elements;
             l.element = elements[0];
@@ -55,13 +58,15 @@ export function water(): StructureQuery {
     };
 }
 
-export function lidangs(): StructureQuery {
+export function atomicHet(): StructureQuery {
     return ctx => {
         const { inputStructure } = ctx;
         const l = StructureElement.create();
 
         const units: Unit[] = [];
         for (const unit of inputStructure.units) {
+            if (unit.kind !== Unit.Kind.Atomic) continue;
+
             l.unit = unit;
             const elements = unit.elements;
             l.element = elements[0];
@@ -82,3 +87,16 @@ export function lidangs(): StructureQuery {
         return StructureSelection.Singletons(inputStructure, new Structure(units));
     };
 }
+
+export function spheres(): StructureQuery {
+    return ctx => {
+        const { inputStructure } = ctx;
+
+        const units: Unit[] = [];
+        for (const unit of inputStructure.units) {
+            if (unit.kind !== Unit.Kind.Spheres) continue;
+            units.push(unit);
+        }
+        return StructureSelection.Singletons(inputStructure, new Structure(units));
+    };
+}

+ 6 - 9
src/mol-model/structure/structure/carbohydrates/compute.ts

@@ -12,12 +12,11 @@ import { Vec3 } from 'mol-math/linear-algebra';
 import PrincipalAxes from 'mol-math/linear-algebra/matrix/principal-axes';
 import { fillSerial } from 'mol-util/array';
 import { ResidueIndex, Model } from '../../model';
-import { ElementSymbol, MoleculeType } from '../../model/types';
-import { getAtomicMoleculeType, getPositionMatrix } from '../../util';
+import { ElementSymbol } from '../../model/types';
+import { getPositionMatrix } from '../../util';
 import StructureElement from '../element';
 import Structure from '../structure';
 import Unit from '../unit';
-import { UnknownSaccharideComponent, SaccharideComponent } from './constants';
 import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink, PartialCarbohydrateElement } from './data';
 import { UnitRings, UnitRing } from '../unit/rings';
 import { ElementIndex } from '../../model/indexing';
@@ -118,8 +117,8 @@ function filterFusedRings(unitRings: UnitRings, rings: UnitRings.Index[] | undef
     }
 }
 
-function getSaccharideComp(compId: string, model: Model): SaccharideComponent {
-    return model.properties.saccharideComponentMap.get(compId) || UnknownSaccharideComponent
+function getSaccharideComp(compId: string, model: Model) {
+    return model.properties.saccharideComponentMap.get(compId)
 }
 
 export function computeCarbohydrates(structure: Structure): Carbohydrates {
@@ -167,9 +166,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                 const { index: residueIndex } = residueIt.move();
 
                 const saccharideComp = getSaccharideComp(label_comp_id.value(residueIndex), model)
-                if (saccharideComp === UnknownSaccharideComponent) {
-                    if (getAtomicMoleculeType(unit.model, residueIndex) !== MoleculeType.saccharide) continue
-                }
+                if (!saccharideComp) continue
 
                 if (!sugarResidueMap) {
                     sugarResidueMap = UnitRings.byFingerprintAndResidue(unit.rings, SugarRingFps);
@@ -402,7 +399,7 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[
         let k: string
         if (fromCarbohydrate) {
             k = terminalLinksKey(unit, anomericCarbon)
-        } else{
+        } else {
             k = terminalLinksKey(elementUnit, elementUnit.elements[elementIndex])
         }
         const e = terminalLinksMap.get(k)

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

@@ -205,7 +205,10 @@ const CommonSaccharideNames: { [k: string]: string[] } = {
         'MLR', // via GlyFinder, tri-saccharide but homomer
     ],
     Man: ['MAN', 'BMA'],
-    Gal: ['GAL', 'GLA'],
+    Gal: [
+        'GAL', 'GLA',
+        'GXL' // via PubChem
+    ],
     Gul: ['GUP', 'GL0'],
     Alt: ['ALT'],
     All: ['ALL', 'AFD'],
@@ -296,6 +299,10 @@ const CommonSaccharideNames: { [k: string]: string[] } = {
     Psi: [],
 }
 
+const UnknownSaccharideNames = [
+    'NGZ', // via CCD
+]
+
 export const SaccharideCompIdMap = (function () {
     const map = new Map<string, SaccharideComponent>()
     for (let i = 0, il = Monosaccharides.length; i < il; ++i) {
@@ -307,6 +314,9 @@ export const SaccharideCompIdMap = (function () {
             }
         }
     }
+    for (let i = 0, il = UnknownSaccharideNames.length; i < il; ++i) {
+        map.set(UnknownSaccharideNames[i], UnknownSaccharideComponent)
+    }
     return map
 })()
 

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

@@ -8,7 +8,7 @@ import { IntMap, SortedArray, Iterator, Segmentation } from 'mol-data/int'
 import { UniqueArray } from 'mol-data/generic'
 import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator'
 import { Model, ElementIndex } from '../model'
-import { sort, arraySwap, hash1, sortArray, hashString } from 'mol-data/util';
+import { sort, arraySwap, hash1, sortArray, hashString, hashFnv32a } from 'mol-data/util';
 import StructureElement from './element'
 import Unit from './unit'
 import { StructureLookup3D } from './util/lookup3d';
@@ -44,9 +44,11 @@ class Structure {
         entityIndices?: ReadonlyArray<EntityIndex>,
         uniqueAtomicResidueIndices?: ReadonlyMap<UUID, ReadonlyArray<ResidueIndex>>,
         hashCode: number,
+        /** Hash based on all unit.id values in the structure, reflecting the units transformation */
+        transformHash: number,
         elementCount: number,
         polymerResidueCount: number,
-    } = { hashCode: -1, elementCount: 0, polymerResidueCount: 0 };
+    } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 };
 
     subsetBuilder(isSorted: boolean) {
         return new StructureSubsetBuilder(this, isSorted);
@@ -74,6 +76,12 @@ class Structure {
         return this.computeHash();
     }
 
+    get transformHash() {
+        if (this._props.transformHash !== -1) return this._props.transformHash;
+        this._props.transformHash = hashFnv32a(this.units.map(u => u.id))
+        return this._props.transformHash;
+    }
+
     private computeHash() {
         let hash = 23;
         for (let i = 0, _i = this.units.length; i < _i; i++) {
@@ -272,6 +280,23 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID,
 namespace Structure {
     export const Empty = new Structure([]);
 
+    /** Represents a single structure */
+    export interface Loci {
+        readonly kind: 'structure-loci',
+        readonly structure: Structure,
+    }
+    export function Loci(structure: Structure): Loci {
+        return { kind: 'structure-loci', structure };
+    }
+
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'structure-loci';
+    }
+
+    export function areLociEqual(a: Loci, b: Loci) {
+        return a.structure === b.structure
+    }
+
     export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); }
 
     /**
@@ -389,6 +414,7 @@ namespace Structure {
         return s.hashCode;
     }
 
+    /** Hash based on all unit.model conformation values in the structure */
     export function conformationHash(s: Structure) {
         return hashString(s.units.map(u => Unit.conformationId(u)).join('|'))
     }
@@ -409,6 +435,13 @@ namespace Structure {
         return true;
     }
 
+    export function areEquivalent(a: Structure, b: Structure) {
+        return a === b || (
+            a.hashCode === b.hashCode &&
+            StructureSymmetry.areTransformGroupsEquivalent(a.unitSymmetryGroups, b.unitSymmetryGroups)
+        )
+    }
+
     export class ElementLocationIterator implements Iterator<StructureElement> {
         private current = StructureElement.create();
         private unitIndex = 0;

+ 9 - 0
src/mol-model/structure/structure/symmetry.ts

@@ -78,6 +78,15 @@ namespace StructureSymmetry {
 
         return ret;
     }
+
+    /** Checks if transform groups are equal up to their unit's transformations */
+    export function areTransformGroupsEquivalent(a: ReadonlyArray<Unit.SymmetryGroup>, b: ReadonlyArray<Unit.SymmetryGroup>) {
+        if (a.length !== b.length) return false
+        for (let i = 0, il = a.length; i < il; ++i) {
+            if (a[i].hashCode !== b[i].hashCode) return false
+        }
+        return true
+    }
 }
 
 function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) {

+ 12 - 8
src/mol-model/structure/structure/unit.ts

@@ -15,7 +15,7 @@ import { UnitRings } from './unit/rings';
 import StructureElement from './element'
 import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing';
 import { IntMap, SortedArray } from 'mol-data/int';
-import { hash2 } from 'mol-data/util';
+import { hash2, hashFnv32a } from 'mol-data/util';
 import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer';
 import { getNucleotideElements } from './util/nucleotide';
 import { GaussianDensityProps, computeUnitGaussianDensityCached } from './unit/gaussian-density';
@@ -48,7 +48,10 @@ namespace Unit {
         readonly units: ReadonlyArray<Unit>
         /** Maps unit.id to index of unit in units array */
         readonly unitIndexMap: IntMap<number>
+        /** Hash based on unit.invariantId which is the same for all units in the group */
         readonly hashCode: number
+        /** Hash based on all unit.id values in the group, reflecting the units transformation*/
+        readonly transformHash: number
     }
 
     function getUnitIndexMap(units: Unit[]) {
@@ -72,7 +75,8 @@ namespace Unit {
                 props.unitIndexMap = getUnitIndexMap(units)
                 return props.unitIndexMap
             },
-            hashCode: hashUnit(units[0])
+            hashCode: hashUnit(units[0]),
+            transformHash: hashFnv32a(units.map(u => u.id))
         }
     }
 
@@ -90,7 +94,7 @@ namespace Unit {
         readonly invariantId: number,
         readonly elements: StructureElement.Set,
         readonly model: Model,
-        readonly conformation: SymmetryOperator.ArrayMapping,
+        readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>,
 
         getChild(elements: StructureElement.Set): Unit,
         applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
@@ -124,7 +128,7 @@ namespace Unit {
         readonly invariantId: number;
         readonly elements: StructureElement.Set;
         readonly model: Model;
-        readonly conformation: SymmetryOperator.ArrayMapping;
+        readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>;
 
         // Reference some commonly accessed things for faster access.
         readonly residueIndex: ArrayLike<ResidueIndex>;
@@ -187,7 +191,7 @@ namespace Unit {
             return computeUnitGaussianDensityCached(this, props, this.props.gaussianDensities, ctx, webgl);
         }
 
-        constructor(id: number, invariantId: number, model: Model, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: AtomicProperties) {
+        constructor(id: number, invariantId: number, model: Model, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: AtomicProperties) {
             this.id = id;
             this.invariantId = invariantId;
             this.model = model;
@@ -229,7 +233,7 @@ namespace Unit {
         readonly invariantId: number;
         readonly elements: StructureElement.Set;
         readonly model: Model;
-        readonly conformation: SymmetryOperator.ArrayMapping;
+        readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>;
 
         readonly coarseElements: CoarseElements;
         readonly coarseConformation: C;
@@ -276,7 +280,7 @@ namespace Unit {
             return computeUnitGaussianDensityCached(this as Unit.Spheres | Unit.Gaussians, props, this.props.gaussianDensities, ctx, webgl); // TODO get rid of casting
         }
 
-        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) {
+        constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties) {
             this.kind = kind;
             this.id = id;
             this.invariantId = invariantId;
@@ -305,7 +309,7 @@ namespace Unit {
         };
     }
 
-    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 {
+    function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties): Unit {
         return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */;
     }
 

+ 1 - 1
src/mol-model/structure/structure/unit/gaussian-density.ts

@@ -18,7 +18,7 @@ export const GaussianDensityParams = {
     resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }),
     radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }),
     smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }),
-    useGpu: PD.Boolean(true),
+    useGpu: PD.Boolean(false),
     ignoreCache: PD.Boolean(false),
 }
 export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams)

+ 35 - 77
src/mol-model/structure/structure/util/boundary.ts

@@ -5,102 +5,60 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Structure from '../structure'
-import Unit from '../unit';
-import { Box3D, Sphere3D, SymmetryOperator } from 'mol-math/geometry';
+import { Box3D, Sphere3D } from 'mol-math/geometry';
+import { BoundaryHelper } from 'mol-math/geometry/boundary-helper';
 import { Vec3 } from 'mol-math/linear-algebra';
-import { SortedArray } from 'mol-data/int';
-import { ElementIndex } from '../../model/indexing';
+import Structure from '../structure';
 
 export type Boundary = { box: Box3D, sphere: Sphere3D }
 
-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 radiusSq = 0
-    let size = 0
-
-    const p = Vec3.zero()
-
-    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)
-    }
-
-    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, max },
-        sphere: { center, radius: Math.sqrt(radiusSq) }
-    }
-}
-
-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()
 
+const boundaryHelper = new BoundaryHelper();
+
 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 { units } = s;
+
+    boundaryHelper.reset(0);
+
+    for (let i = 0, _i = units.length; i < _i; i++) {
+        const u = units[i];
+        const invariantBoundary = u.lookup3d.boundary;
+        const o = u.conformation.operator;
+
+        if (o.isIdentity) {
+            Vec3.min(min, min, invariantBoundary.box.min);
+            Vec3.max(max, max, invariantBoundary.box.max);
 
-    const boundaryMap: Map<number, Boundary> = new Map()
-    function getInvariantBoundary(u: Unit) {
-        let boundary: Boundary
-        if (boundaryMap.has(u.invariantId)) {
-            boundary = boundaryMap.get(u.invariantId)!
+            boundaryHelper.boundaryStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
         } else {
-            boundary = computeInvariantUnitBoundary(u)
-            boundaryMap.set(u.invariantId, boundary)
+            Box3D.transform(tmpBox, invariantBoundary.box, o.matrix);
+            Vec3.min(min, min, tmpBox.min);
+            Vec3.max(max, max, tmpBox.max);
+
+            Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix);
+            boundaryHelper.boundaryStep(tmpSphere.center, tmpSphere.radius);
         }
-        return boundary
     }
 
-    let radius = 0
-    let size = 0
+    boundaryHelper.finishBoundaryStep();
 
     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)
+        const u = units[i];
+        const invariantBoundary = u.lookup3d.boundary;
+        const o = u.conformation.operator;
 
-    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
+        if (o.isIdentity) {
+            boundaryHelper.extendStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius);
+        } else {
+            Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix);
+            boundaryHelper.extendStep(tmpSphere.center, tmpSphere.radius);
+        }
     }
 
-    return { box: { min, max }, sphere: { center, radius } }
+    return { box: { min, max }, sphere: boundaryHelper.getSphere() };
 }

+ 2 - 0
src/mol-plugin/behavior.ts

@@ -13,6 +13,7 @@ import * as StaticMisc from './behavior/static/misc'
 
 import * as DynamicRepresentation from './behavior/dynamic/representation'
 import * as DynamicCamera from './behavior/dynamic/camera'
+import * as DynamicCustomProps from './behavior/dynamic/custom-props'
 
 export const BuiltInPluginBehaviors = {
     State: StaticState,
@@ -24,4 +25,5 @@ export const BuiltInPluginBehaviors = {
 export const PluginBehaviors = {
     Representation: DynamicRepresentation,
     Camera: DynamicCamera,
+    CustomProps: DynamicCustomProps
 }

+ 1 - 1
src/mol-plugin/behavior/behavior.ts

@@ -43,7 +43,7 @@ namespace PluginBehavior {
 
     export function create<P>(params: CreateParams<P>) {
         // TODO: cache groups etc
-        return PluginStateTransform.Create<Root, Behavior, P>({
+        return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({
             name: params.name,
             display: params.display,
             from: [Root],

+ 81 - 0
src/mol-plugin/behavior/dynamic/custom-props.ts

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

+ 4 - 4
src/mol-plugin/behavior/dynamic/representation.ts

@@ -19,8 +19,8 @@ export const HighlightLoci = PluginBehavior.create({
                 if (!this.ctx.canvas3d) return;
 
                 if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight);
+                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight, prevRepr);
+                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight, current.repr);
                     prevLoci = current.loci;
                     prevRepr = current.repr;
                 }
@@ -38,8 +38,8 @@ export const SelectLoci = PluginBehavior.create({
             this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => {
                 if (!this.ctx.canvas3d) return;
                 if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) {
-                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect);
-                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Select);
+                    this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect, prevRepr);
+                    this.ctx.canvas3d.mark(current.loci, MarkerAction.Select, current.repr);
                     prevLoci = current.loci;
                     prevRepr = current.repr;
                 } else {

+ 4 - 5
src/mol-plugin/behavior/static/representation.ts

@@ -19,10 +19,10 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
         updateVisibility(e, e.obj.data);
+        e.obj.data.setState({ syncManually: true });
         ctx.canvas3d.add(e.obj.data);
         // TODO: only do this if there were no representations previously
         ctx.canvas3d.resetCamera();
-        ctx.canvas3d.requestDraw(true);
     });
     events.object.updated.subscribe(e => {
         if (e.oldObj && SO.isRepresentation3D(e.oldObj)) {
@@ -34,11 +34,10 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
         if (!SO.isRepresentation3D(e.obj)) return;
 
         updateVisibility(e, e.obj.data);
-
         if (e.action === 'recreate') {
-            ctx.canvas3d.add(e.obj.data);
-            ctx.canvas3d.requestDraw(true);
+            e.obj.data.setState({ syncManually: true });
         }
+        ctx.canvas3d.add(e.obj.data);
     });
     events.object.removed.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
@@ -58,5 +57,5 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
 }
 
 function updateVisibility(e: State.ObjectEvent, r: Representation<any>) {
-    r.setVisibility(!e.state.cellStates.get(e.ref).isHidden);
+    r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden });
 }

+ 26 - 1
src/mol-plugin/behavior/static/state.ts

@@ -8,7 +8,9 @@ import { PluginCommands } from '../../command';
 import { PluginContext } from '../../context';
 import { StateTree, Transform, State } from 'mol-state';
 import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
-import { PluginStateObject as SO } from '../../state/objects';
+import { PluginStateObject as SO, PluginStateObject } from '../../state/objects';
+import { EmptyLoci, EveryLoci } from 'mol-model/loci';
+import { Structure } from 'mol-model/structure';
 
 export function registerDefault(ctx: PluginContext) {
     SyncBehaviors(ctx);
@@ -18,6 +20,8 @@ export function registerDefault(ctx: PluginContext) {
     RemoveObject(ctx);
     ToggleExpanded(ctx);
     ToggleVisibility(ctx);
+    Highlight(ctx);
+    ClearHighlight(ctx);
     Snapshots(ctx);
 }
 
@@ -75,6 +79,27 @@ function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State
     ctx.state.updateCellState(t.ref, { isHidden: ctx.value });
 }
 
+// TODO make isHighlighted and isSelect part of StateObjectCell.State and subscribe from there???
+// TODO select structures of subtree
+// TODO should also work for volumes and shapes
+export function Highlight(ctx: PluginContext) {
+    PluginCommands.State.Highlight.subscribe(ctx, ({ state, ref }) => {
+        const cell = state.select(ref)[0]
+        const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined
+        if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) {
+            ctx.behaviors.canvas.highlightLoci.next({ loci: Structure.Loci(cell.obj.data) })
+        } else if (repr) {
+            ctx.behaviors.canvas.highlightLoci.next({ loci: EveryLoci, repr })
+        }
+    });
+}
+
+export function ClearHighlight(ctx: PluginContext) {
+    PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => {
+        ctx.behaviors.canvas.highlightLoci.next({ loci: EmptyLoci })
+    });
+}
+
 export function Snapshots(ctx: PluginContext) {
     PluginCommands.State.Snapshots.Clear.subscribe(ctx, () => {
         ctx.state.snapshots.clear();

+ 2 - 0
src/mol-plugin/command.ts

@@ -22,6 +22,8 @@ export const PluginCommands = {
 
         ToggleExpanded: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
         ToggleVisibility: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
+        Highlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
+        ClearHighlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }),
 
         Snapshots: {
             Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }),

+ 22 - 14
src/mol-plugin/context.ts

@@ -23,6 +23,8 @@ import { PluginState } from './state';
 import { TaskManager } from './util/task-manager';
 import { Color } from 'mol-util/color';
 import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager';
+import { ajaxGet } from 'mol-util/data-source';
+import { CustomPropertyRegistry } from './util/custom-prop-registry';
 
 export class PluginContext {
     private disposed = false;
@@ -57,13 +59,6 @@ export class PluginContext {
         }
     };
 
-    readonly lociLabels: LociLabelManager;
-
-    readonly structureReprensentation = {
-        registry: new StructureRepresentationRegistry(),
-        themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext
-    }
-
     readonly behaviors = {
         canvas: {
             highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }),
@@ -74,6 +69,14 @@ export class PluginContext {
 
     readonly canvas3d: Canvas3D;
 
+    readonly lociLabels: LociLabelManager;
+
+    readonly structureRepresentation = {
+        registry: new StructureRepresentationRegistry(),
+        themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext
+    }
+
+    readonly customModelProperties = new CustomPropertyRegistry();
 
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
@@ -82,23 +85,28 @@ export class PluginContext {
             this.canvas3d.animate();
             return true;
         } catch (e) {
-            this.log(LogEntry.error('' + e));
+            this.log.error('' + e);
             console.error(e);
             return false;
         }
     }
 
-    log(e: LogEntry) {
-        this.events.log.next(e);
-    }
+    readonly log = {
+        entry: (e: LogEntry) => this.events.log.next(e),
+        error: (msg: string) => this.events.log.next(LogEntry.error(msg)),
+        message: (msg: string) => this.events.log.next(LogEntry.message(msg)),
+        info: (msg: string) => this.events.log.next(LogEntry.info(msg)),
+        warn: (msg: string) => this.events.log.next(LogEntry.warning(msg)),
+    };
 
     /**
      * This should be used in all transform related request so that it could be "spoofed" to allow
      * "static" access to resources.
      */
-    async fetch(url: string, type: 'string' | 'binary' = 'string'): Promise<string | Uint8Array> {
-        const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' });
-        return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
+    fetch(url: string, type: 'string' | 'binary' = 'string'): Task<string | Uint8Array> {
+        return ajaxGet({ url, type });
+        // const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' });
+        // return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
     }
 
     runTask<T>(task: Task<T>) {

+ 7 - 5
src/mol-plugin/index.ts

@@ -10,10 +10,9 @@ import * as React from 'react';
 import * as ReactDOM from 'react-dom';
 import { PluginCommands } from './command';
 import { PluginSpec } from './spec';
-import { CreateStructureFromPDBe } from './state/actions/basic';
+import { DownloadStructure, CreateComplexRepresentation, OpenStructure } from './state/actions/basic';
 import { StateTransforms } from './state/transforms';
 import { PluginBehaviors } from './behavior';
-import { LogEntry } from 'mol-util/log-entry';
 
 function getParam(name: string, regex: string): string {
     let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
@@ -22,7 +21,9 @@ function getParam(name: string, regex: string): string {
 
 const DefaultSpec: PluginSpec = {
     actions: [
-        PluginSpec.Action(CreateStructureFromPDBe),
+        PluginSpec.Action(DownloadStructure),
+        PluginSpec.Action(OpenStructure),
+        PluginSpec.Action(CreateComplexRepresentation),
         PluginSpec.Action(StateTransforms.Data.Download),
         PluginSpec.Action(StateTransforms.Data.ParseCif),
         PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel),
@@ -34,7 +35,8 @@ const DefaultSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
-        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 })
+        PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }),
+        PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: false })
     ]
 }
 
@@ -53,7 +55,7 @@ async function trySetSnapshot(ctx: PluginContext) {
         if (!snapshotUrl) return;
         await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl })
     } catch (e) {
-        ctx.log(LogEntry.error('Failed to load snapshot.'));
+        ctx.log.error('Failed to load snapshot.');
         console.warn('Failed to load snapshot', e);
     }
 }

+ 1 - 1
src/mol-plugin/skin/base/components/controls-base.scss

@@ -83,7 +83,7 @@
     width: 100%;
     background: $msp-form-control-background;
     color: $font-color;
-    border: none !important;
+    border: none; // !important;
     padding: 0 $control-spacing;   
     line-height: $row-height - 2px;  
     height: $row-height;

+ 51 - 0
src/mol-plugin/skin/base/components/controls.scss

@@ -110,6 +110,57 @@
     // }
 }
 
+.msp-slider2 {
+    > div:first-child {
+        position: absolute;
+        height: $row-height;
+        line-height: $row-height;
+        text-align: center;
+        left: 0;
+        width: 25px;
+        top: 0;
+        bottom: 0;
+        font-size: 80%;
+    }
+    > div:nth-child(2) {
+        position: absolute;
+        top: 0;
+        left: 0;
+        bottom: 0;
+        right: 25px;
+        width: 100%;
+        padding-left: 20px;
+        padding-right: 25px;
+        display: table;
+        
+        > div {
+            height: $row-height;
+            display: table-cell;
+            vertical-align: middle;
+            padding: 0 ($control-spacing + 4px);
+        }
+    }
+    > div:last-child {
+        position: absolute;
+        height: $row-height;
+        line-height: $row-height;
+        text-align: center;
+        right: 0;
+        width: 25px;
+        top: 0;
+        bottom: 0;
+        font-size: 80%;
+    }
+    
+    // input[type=text] {
+    //     text-align: right;
+    // }
+    
+    // input[type=range] {
+    //     width: 100%;
+    // }
+}
+
 .msp-toggle-color-picker {
     button {
         border: $control-spacing solid $msp-form-control-background !important;

+ 3 - 0
src/mol-plugin/skin/base/components/temp.scss

@@ -9,6 +9,9 @@
     text-align: center;
     font-weight: bold;
     background: $default-background;
+    // border-right: $control-spacing solid $entity-color-Group; // TODO separate color
+    border-top: 1px solid $entity-color-Group; // TODO separate color
+    // border-bottom: 1px solid $entity-color-Group; // TODO separate color
 }
 
 .msp-btn-row-group {

+ 1 - 0
src/mol-plugin/skin/base/components/transformer.scss

@@ -44,6 +44,7 @@
 
 .msp-transform-header {
     position: relative;
+    border-top: 1px solid $entity-color-Behaviour; // TODO: separate color
 
     > button {
         text-align: left;

+ 152 - 71
src/mol-plugin/state/actions/basic.ts

@@ -11,81 +11,162 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateSelection } from 'mol-state/state/selection';
 import { CartoonParams } from 'mol-repr/structure/representation/cartoon';
 import { BallAndStickParams } from 'mol-repr/structure/representation/ball-and-stick';
+import { Download } from '../transforms/data';
+import { StateTree, Transformer } from 'mol-state';
+import { StateTreeBuilder } from 'mol-state/tree/builder';
+import { PolymerIdColorThemeParams } from 'mol-theme/color/polymer-id';
+import { UniformSizeThemeParams } from 'mol-theme/size/uniform';
+import { ElementSymbolColorThemeParams } from 'mol-theme/color/element-symbol';
 
-export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root, void, { id: string }>({
-    from: [PluginStateObject.Root],
-    display: {
-        name: 'Entry from PDBe',
-        description: 'Download a structure from PDBe and create its default Assembly and visual'
-    },
-    params: () => ({ id: PD.Text('1grm', { label: 'PDB id' }) }),
-    apply({ params, state }) {
-        const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;
-        const b = state.build();
-
-        // const query = MolScriptBuilder.struct.generator.atomGroups({
-        //     // 'atom-test': MolScriptBuilder.core.rel.eq([
-        //     //     MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
-        //     //     MolScriptBuilder.es('C')
-        //     // ]),
-        //     'residue-test': MolScriptBuilder.core.rel.eq([
-        //         MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
-        //         'ALA'
-        //     ])
-        // });
-
-        const root = b.toRoot()
-            .apply(StateTransforms.Data.Download, { url })
-            .apply(StateTransforms.Data.ParseCif)
-            .apply(StateTransforms.Model.TrajectoryFromMmCif, {})
-            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.StructureAssemblyFromModel);
-
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'sequence' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) } });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'ligands' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) } });
-        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
-            .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } } });
-
-        return state.update(root.getTree());
+// TODO: "structure parser provider"
+
+export { DownloadStructure }
+type DownloadStructure = typeof DownloadStructure
+const DownloadStructure = StateAction.build({
+    from: PluginStateObject.Root,
+    display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' },
+    params: {
+        source: PD.MappedStatic('bcif-static', {
+            'pdbe-updated': PD.Group({
+                id: PD.Text('1cbs', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'rcsb': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'bcif-static': PD.Group({
+                id: PD.Text('1tqn', { label: 'Id' }),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true }),
+            'url': PD.Group({
+                url: PD.Text(''),
+                isBinary: PD.Boolean(false),
+                supportProps: PD.Boolean(false)
+            }, { isFlat: true })
+        }, {
+            options: [
+                ['pdbe-updated', 'PDBe Updated'],
+                ['rcsb', 'RCSB'],
+                ['bcif-static', 'BinaryCIF (static PDBe Updated)'],
+                ['url', 'URL']
+            ]
+        })
+    }
+})(({ params, state }) => {
+    const b = state.build();
+    const src = params.source;
+    let url: Transformer.Params<Download>;
+
+    switch (src.name) {
+        case 'url':
+            url = src.params;
+            break;
+        case 'pdbe-updated':
+            url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` };
+            break;
+        case 'rcsb':
+            url = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` };
+            break;
+        case 'bcif-static':
+            url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` };
+            break;
+        default: throw new Error(`${(src as any).name} not supported.`);
     }
+
+    const data = b.toRoot().apply(StateTransforms.Data.Download, url);
+    return state.update(createStructureTree(data, params.source.params.supportProps));
 });
 
-export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void, { action: 'advance' | 'reset', by?: number }>({
-    from: [],
-    display: {
-        name: 'Update Trajectory'
-    },
-    params: () => ({
-        action: PD.Select('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
-        by: PD.Numeric(1, { min: -1, max: 1, step: 1 }, { isOptional: true })
-    }),
-    apply({ params, state }) {
-        const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-            .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
-
-        const update = state.build();
-
-        if (params.action === 'reset') {
-            for (const m of models) {
-                update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                    () => ({ modelIndex: 0}));
-            }
-        } else {
-            for (const m of models) {
-                const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
-                if (!parent || !parent.obj) continue;
-                const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-                update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                    old => {
-                        let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                        if (modelIndex < 0) modelIndex += traj.data.length;
-                        return { modelIndex };
-                    });
-            }
-        }
+export const OpenStructure = StateAction.build({
+    display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' },
+    from: PluginStateObject.Root,
+    params: { file: PD.File({ accept: '.cif,.bcif' }) }
+})(({ params, state }) => {
+    const b = state.build();
+    const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) });
+    return state.update(createStructureTree(data, false));
+});
+
+function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree {
+    let root = b
+        .apply(StateTransforms.Data.ParseCif)
+        .apply(StateTransforms.Model.TrajectoryFromMmCif, {})
+        .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 });
+
+    if (supportProps) {
+        // TODO: implement automatic default property assigment in State.update
+        root = root.apply(StateTransforms.Model.CustomModelProperties, { properties: [] });
+    }
+    root = root.apply(StateTransforms.Model.StructureAssemblyFromModel);
+
+    complexRepresentation(root);
 
-        return state.update(update);
+    return root.getTree();
+}
+
+function complexRepresentation(root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) {
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D, {
+            type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) },
+            colorTheme: { name: 'polymer-id', params: PD.getDefaultValues(PolymerIdColorThemeParams) },
+            sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) },
+        });
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D, {
+            type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) },
+            colorTheme: { name: 'element-symbol', params: PD.getDefaultValues(ElementSymbolColorThemeParams) },
+            sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) },
+        });
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+        .apply(StateTransforms.Representation.StructureRepresentation3D, {
+            type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } },
+            colorTheme: { name: 'element-symbol', params: PD.getDefaultValues(ElementSymbolColorThemeParams) },
+            sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) },
+        })
+    root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' });
+    // TODO: create spheres visual
+}
+
+export const CreateComplexRepresentation = StateAction.build({
+    display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' },
+    from: PluginStateObject.Molecule.Structure
+})(({ ref, state }) => {
+    const root = state.build().to(ref);
+    complexRepresentation(root);
+    return state.update(root.getTree());
+});
+
+export const UpdateTrajectory = StateAction.build({
+    display: { name: 'Update Trajectory' },
+    params: {
+        action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]),
+        by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
+    }
+})(({ params, state }) => {
+    const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model)
+        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+
+    const update = state.build();
+
+    if (params.action === 'reset') {
+        for (const m of models) {
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                () => ({ modelIndex: 0 }));
+        }
+    } else {
+        for (const m of models) {
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            if (!parent || !parent.obj) continue;
+            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
+                old => {
+                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                    if (modelIndex < 0) modelIndex += traj.data.length;
+                    return { modelIndex };
+                });
+        }
     }
+
+    return state.update(update);
 });

+ 7 - 3
src/mol-plugin/state/objects.ts

@@ -46,8 +46,6 @@ export namespace PluginStateObject {
     export namespace Data {
         export class String extends Create<string>({ name: 'String Data', typeClass: 'Data', }) { }
         export class Binary extends Create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data' }) { }
-        export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
-        export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
 
         // TODO
         // export class MultipleRaw extends Create<{
@@ -55,6 +53,11 @@ export namespace PluginStateObject {
         // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { }
     }
 
+    export namespace Format {
+        export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { }
+        export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { }
+    }
+
     export namespace Molecule {
         export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { }
         export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { }
@@ -69,5 +72,6 @@ export namespace PluginStateObject {
 }
 
 export namespace PluginStateTransform {
-    export const Create = Transformer.factory('ms-plugin');
+    export const CreateBuiltIn = Transformer.factory('ms-plugin');
+    export const BuiltIn = Transformer.builderFactory('ms-plugin');
 }

+ 47 - 20
src/mol-plugin/state/transforms/data.ts

@@ -11,26 +11,24 @@ import CIF from 'mol-io/reader/cif'
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Transformer } from 'mol-state';
+import { readFromFile } from 'mol-util/data-source';
 
 export { Download }
-namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
-const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({
+type Download = typeof Download
+const Download = PluginStateTransform.BuiltIn({
     name: 'download',
-    display: {
-        name: 'Download',
-        description: 'Download string or binary data from the specified URL'
-    },
+    display: { name: 'Download', description: 'Download string or binary data from the specified URL' },
     from: [SO.Root],
     to: [SO.Data.String, SO.Data.Binary],
-    params: () => ({
+    params: {
         url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }),
-        label: PD.Text('', { isOptional: true }),
-        isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)', isOptional: true })
-    }),
+        label: PD.makeOptional(PD.Text('')),
+        isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' }))
+    }
+})({
     apply({ params: p }, globalCtx: PluginContext) {
         return Task.create('Download', async ctx => {
-            // TODO: track progress
-            const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string');
+            const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string').runInContext(ctx);
             return p.isBinary
                 ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url })
                 : new SO.Data.String(data as string, { label: p.label ? p.label : p.url });
@@ -46,21 +44,50 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B
     }
 });
 
+export { ReadFile }
+type ReadFile = typeof ReadFile
+const ReadFile = PluginStateTransform.BuiltIn({
+    name: 'read-file',
+    display: { name: 'Read File', description: 'Read string or binary data from the specified file' },
+    from: SO.Root,
+    to: [SO.Data.String, SO.Data.Binary],
+    params: {
+        file: PD.File(),
+        label: PD.makeOptional(PD.Text('')),
+        isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' }))
+    }
+})({
+    apply({ params: p }) {
+        return Task.create('Open File', async ctx => {
+            const data = await readFromFile(p.file, p.isBinary ? 'binary' : 'string').runInContext(ctx);
+            return p.isBinary
+                ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.file.name })
+                : new SO.Data.String(data as string, { label: p.label ? p.label : p.file.name });
+        });
+    },
+    update({ oldParams, newParams, b }) {
+        if (oldParams.label !== newParams.label) {
+            (b.label as string) = newParams.label || oldParams.file.name;
+            return Transformer.UpdateResult.Updated;
+        }
+        return Transformer.UpdateResult.Unchanged;
+    },
+    isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' })
+});
+
 export { ParseCif }
-namespace ParseCif { export interface Params { } }
-const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({
+type ParseCif = typeof ParseCif
+const ParseCif = PluginStateTransform.BuiltIn({
     name: 'parse-cif',
-    display: {
-        name: 'Parse CIF',
-        description: 'Parse CIF from String or Binary data'
-    },
+    display: { name: 'Parse CIF', description: 'Parse CIF from String or Binary data' },
     from: [SO.Data.String, SO.Data.Binary],
-    to: [SO.Data.Cif],
+    to: SO.Format.Cif
+})({
     apply({ a }) {
         return Task.create('Parse CIF', async ctx => {
             const parsed = await (SO.Data.String.is(a) ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx);
             if (parsed.isError) throw new Error(parsed.message);
-            return new SO.Data.Cif(parsed.result);
+            return new SO.Format.Cif(parsed.result);
         });
     }
 });

+ 84 - 66
src/mol-plugin/state/transforms/model.ts

@@ -6,31 +6,29 @@
 
 import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } from '../objects';
-import { Task } from 'mol-task';
+import { Task, RuntimeContext } from 'mol-task';
 import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import Expression from 'mol-script/language/expression';
 import { compile } from 'mol-script/runtime/query/compiler';
 import { MolScriptBuilder } from 'mol-script/language/builder';
 import { StateObject } from 'mol-state';
+import { PluginContext } from 'mol-plugin/context';
 
 export { TrajectoryFromMmCif }
-namespace TrajectoryFromMmCif { export interface Params { blockHeader?: string } }
-const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule.Trajectory, TrajectoryFromMmCif.Params>({
+type TrajectoryFromMmCif = typeof TrajectoryFromMmCif
+const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({
     name: 'trajectory-from-mmcif',
-    display: {
-        name: 'Models from mmCIF',
-        description: 'Identify and create all separate models in the specified CIF data block'
-    },
-    from: [SO.Data.Cif],
-    to: [SO.Molecule.Trajectory],
+    display: { name: 'Trajectory from mmCIF', description: 'Identify and create all separate models in the specified CIF data block' },
+    from: SO.Format.Cif,
+    to: SO.Molecule.Trajectory,
     params(a) {
         const { blocks } = a.data;
-        if (blocks.length === 0) return { };
         return {
-            blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })
+            blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }))
         };
-    },
+    }
+})({
     isApplicable: a => a.data.blocks.length > 0,
     apply({ a, params }) {
         return Task.create('Parse mmCIF', async ctx => {
@@ -47,16 +45,14 @@ const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule
 
 export { ModelFromTrajectory }
 const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1;
-namespace ModelFromTrajectory { export interface Params { modelIndex: number } }
-const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, SO.Molecule.Model, ModelFromTrajectory.Params>({
+type ModelFromTrajectory = typeof ModelFromTrajectory
+const ModelFromTrajectory = PluginStateTransform.BuiltIn({
     name: 'model-from-trajectory',
-    display: {
-        name: 'Model from Trajectory',
-        description: 'Create a molecular structure from the specified model.'
-    },
-    from: [SO.Molecule.Trajectory],
-    to: [SO.Molecule.Model],
-    params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }),
+    display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' },
+    from: SO.Molecule.Trajectory,
+    to: SO.Molecule.Model,
+    params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) })
+})({
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
@@ -67,15 +63,13 @@ const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory,
 });
 
 export { StructureFromModel }
-namespace StructureFromModel { export interface Params { } }
-const StructureFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureFromModel.Params>({
+type StructureFromModel = typeof StructureFromModel
+const StructureFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-from-model',
-    display: {
-        name: 'Structure from Model',
-        description: 'Create a molecular structure from the specified model.'
-    },
-    from: [SO.Molecule.Model],
-    to: [SO.Molecule.Structure],
+    display: { name: 'Structure from Model', description: 'Create a molecular structure from the specified model.' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Structure
+})({
     apply({ a }) {
         let s = Structure.ofModel(a.data);
         const label = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` };
@@ -88,29 +82,33 @@ function structureDesc(s: Structure) {
 }
 
 export { StructureAssemblyFromModel }
-namespace StructureAssemblyFromModel { export interface Params { /** if not specified, use the 1st */ id?: string } }
-const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureAssemblyFromModel.Params>({
+type StructureAssemblyFromModel = typeof StructureAssemblyFromModel
+const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({
     name: 'structure-assembly-from-model',
-    display: {
-        name: 'Structure Assembly',
-        description: 'Create a molecular structure assembly.'
-    },
-    from: [SO.Molecule.Model],
-    to: [SO.Molecule.Structure],
+    display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Structure,
     params(a) {
         const model = a.data;
         const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]);
-        return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) };
-    },
-    apply({ a, params }) {
+        return { id: PD.makeOptional(PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' })) };
+    }
+})({
+    apply({ a, params }, plugin: PluginContext) {
         return Task.create('Build Assembly', async ctx => {
-            let id = params.id;
+            let id = (params.id || '').trim();
             const model = a.data;
             if (!id && model.symmetry.assemblies.length) id = model.symmetry.assemblies[0].id;
-            const asm = ModelSymmetry.findAssembly(model, id || '');
-            if (!asm) throw new Error(`Assembly '${id}' not found`);
+            const asm = ModelSymmetry.findAssembly(model, id);
+            if (id && !asm) throw new Error(`Assembly '${id}' not found`);
 
             const base = Structure.ofModel(model);
+            if (!asm) {
+                plugin.log.warn(`Model '${a.label}' has no assembly, returning default structure.`);
+                const label = { label: a.data.label, description: structureDesc(base) };
+                return new SO.Molecule.Structure(base, label);
+            }
+
             const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx);
             const label = { label: `Assembly ${id}`, description: structureDesc(s) };
             return new SO.Molecule.Structure(s, label);
@@ -119,19 +117,17 @@ const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model
 });
 
 export { StructureSelection }
-namespace StructureSelection { export interface Params { query: Expression, label?: string } }
-const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureSelection.Params>({
+type StructureSelection = typeof StructureSelection
+const StructureSelection = PluginStateTransform.BuiltIn({
     name: 'structure-selection',
-    display: {
-        name: 'Structure Selection',
-        description: 'Create a molecular structure from the specified model.'
-    },
-    from: [SO.Molecule.Structure],
-    to: [SO.Molecule.Structure],
-    params: () => ({
+    display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified model.' },
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure,
+    params: {
         query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }),
-        label: PD.Text('', { isOptional: true, isHidden: true })
-    }),
+        label: PD.makeOptional(PD.Text('', { isHidden: true }))
+    }
+})({
     apply({ a, params }) {
         // TODO: use cache, add "update"
         const compiled = compile<Sel>(params.query);
@@ -143,25 +139,25 @@ const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO
 });
 
 export { StructureComplexElement }
-namespace StructureComplexElement { export interface Params { type: 'sequence' | 'water' | 'ligands' } }
-const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureComplexElement.Params>({
+namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' }
+type StructureComplexElement = typeof StructureComplexElement
+const StructureComplexElement = PluginStateTransform.BuiltIn({
     name: 'structure-complex-element',
-    display: {
-        name: 'Complex Element',
-        description: 'Create a molecular structure from the specified model.'
-    },
-    from: [SO.Molecule.Structure],
-    to: [SO.Molecule.Structure],
-    params: () => ({ type: PD.Text('sequence', { isHidden: true }) }),
+    display: { name: 'Complex Element', description: 'Create a molecular structure from the specified model.' },
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Structure,
+    params: { type: PD.Text<StructureComplexElement.Types>('atomic-sequence', { isHidden: true }) }
+})({
     apply({ a, params }) {
         // TODO: update function.
 
         let query: StructureQuery, label: string;
         switch (params.type) {
-            case 'sequence': query = Queries.internal.sequence(); label = 'Sequence'; break;
+            case 'atomic-sequence': query = Queries.internal.atomicSequence(); label = 'Sequence'; break;
             case 'water': query = Queries.internal.water(); label = 'Water'; break;
-            case 'ligands': query = Queries.internal.lidangs(); label = 'Ligands'; break;
-            default: throw new Error(`${params.type} is a valid complex element.`);
+            case 'atomic-het': query = Queries.internal.atomicHet(); label = 'HET Groups/Ligands'; break;
+            case 'spheres': query = Queries.internal.spheres(); label = 'Coarse Spheres'; break;
+            default: throw new Error(`${params.type} is a not valid complex element.`);
         }
 
         const result = query(new QueryContext(a.data));
@@ -172,3 +168,25 @@ const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structur
     }
 });
 
+export { CustomModelProperties }
+type CustomModelProperties = typeof CustomModelProperties
+const CustomModelProperties = PluginStateTransform.BuiltIn({
+    name: 'custom-model-properties',
+    display: { name: 'Custom Model Properties' },
+    from: SO.Molecule.Model,
+    to: SO.Molecule.Model,
+    params: (a, ctx: PluginContext) => ({ properties: ctx.customModelProperties.getSelect(a.data) })
+})({
+    apply({ a, params }, ctx: PluginContext) {
+        return Task.create('Custom Props', async taskCtx => {
+            await attachProps(a.data, ctx, taskCtx, params.properties);
+            return new SO.Molecule.Model(a.data, { label: 'Props', description: `${params.properties.length} Selected` });
+        });
+    }
+});
+async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) {
+    for (const name of names) {
+        const p = ctx.customModelProperties.get(name);
+        await p.attach(model).runInContext(taskCtx);
+    }
+}

+ 38 - 19
src/mol-plugin/state/transforms/representation.ts

@@ -2,6 +2,7 @@
  * 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 { Transformer } from 'mol-state';
@@ -10,37 +11,55 @@ import { PluginStateTransform } from '../objects';
 import { PluginStateObject as SO } from '../objects';
 import { PluginContext } from 'mol-plugin/context';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { createTheme } from 'mol-theme/theme';
 
 export { StructureRepresentation3D }
-namespace StructureRepresentation3D {
-    export interface Params {
-        type: { name: string, params: any /** todo is there "common type" */ },
-    }
-}
-const StructureRepresentation3D = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Representation3D, StructureRepresentation3D.Params>({
+type StructureRepresentation3D = typeof StructureRepresentation3D
+const StructureRepresentation3D = PluginStateTransform.BuiltIn({
     name: 'structure-representation-3d',
-    display: { name: '3D Representation' },
-    from: [SO.Molecule.Structure],
-    to: [SO.Molecule.Representation3D],
+    display: '3D Representation',
+    from: SO.Molecule.Structure,
+    to: SO.Molecule.Representation3D,
     params: (a, ctx: PluginContext) => ({
-        type: PD.Mapped(
-            ctx.structureReprensentation.registry.default.name,
-            ctx.structureReprensentation.registry.types,
-            name => PD.Group<any>(ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data)))
-    }),
+        type: PD.Mapped<any>(
+            ctx.structureRepresentation.registry.default.name,
+            ctx.structureRepresentation.registry.types,
+            name => PD.Group<any>(ctx.structureRepresentation.registry.get(name).getParams(ctx.structureRepresentation.themeCtx, a.data))),
+        colorTheme: PD.Mapped<any>(
+            // TODO how to get a default color theme dependent on the repr type?
+            ctx.structureRepresentation.themeCtx.colorThemeRegistry.default.name,
+            ctx.structureRepresentation.themeCtx.colorThemeRegistry.types,
+            name => PD.Group<any>(ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(name).getParams({ structure: a.data }))
+        ),
+        sizeTheme: PD.Mapped<any>(
+            // TODO how to get a default size theme dependent on the repr type?
+            ctx.structureRepresentation.themeCtx.sizeThemeRegistry.default.name,
+            ctx.structureRepresentation.themeCtx.sizeThemeRegistry.types,
+            name => PD.Group<any>(ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(name).getParams({ structure: a.data }))
+        )
+    })
+})({
+    canAutoUpdate({ oldParams, newParams }) {
+        // TODO: allow for small molecules
+        return oldParams.type.name === newParams.type.name;
+    },
     apply({ a, params }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
-            const provider = plugin.structureReprensentation.registry.get(params.type.name)
-            const repr = provider.factory(provider.getParams)
-            await repr.createOrUpdate({ webgl: plugin.canvas3d.webgl, ...plugin.structureReprensentation.themeCtx }, params.type.params || {}, a.data).runInContext(ctx);
+            const provider = plugin.structureRepresentation.registry.get(params.type.name)
+            const props = params.type.params || {}
+            const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.structureRepresentation.themeCtx }, provider.getParams)
+            repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, params))
+            // TODO set initial state, repr.setState({})
+            await repr.createOrUpdate(props, a.data).runInContext(ctx);
             return new SO.Molecule.Representation3D(repr, { label: provider.label });
         });
     },
     update({ a, b, oldParams, newParams }, plugin: PluginContext) {
         return Task.create('Structure Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate;
-
-            await b.data.createOrUpdate({ webgl: plugin.canvas3d.webgl, ...plugin.structureReprensentation.themeCtx }, { ...b.data.props, ...newParams.type.params }, a.data).runInContext(ctx);
+            const props = { ...b.data.props, ...newParams.type.params }
+            b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams))
+            await b.data.createOrUpdate(props, a.data).runInContext(ctx);
             return Transformer.UpdateResult.Updated;
         });
     }

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

@@ -45,7 +45,7 @@ export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArr
     }
 
     render() {
-        return <div>
+        return <div style={{ textAlign: 'right' }}>
             {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)}
         </div>
     }

+ 75 - 25
src/mol-plugin/ui/controls/parameters.tsx

@@ -9,12 +9,15 @@ import * as React from 'react'
 
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { camelCaseToWords } from 'mol-util/string';
-import { ColorNames } from 'mol-util/color/tables';
+import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables';
 import { Color } from 'mol-util/color';
 import { Slider } from './slider';
 import { Vec2 } from 'mol-math/linear-algebra';
 import LineGraphComponent from './LineGraph/LineGraphComponent';
 
+import { Slider, Slider2 } from './slider';
+
+
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
     values: any,
@@ -49,14 +52,17 @@ function controlFor(param: PD.Any): ParamControl | undefined {
         case 'multi-select': return MultiSelectControl;
         case 'color': return ColorControl;
         case 'vec3': return Vec3Control;
+        case 'file': return FileControl;
         case 'select': return SelectControl;
         case 'text': return TextControl;
-        case 'interval': return IntervalControl;
+        case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined'
+        ? BoundedIntervalControl : IntervalControl;
         case 'group': return GroupControl;
         case 'mapped': return MappedControl;
         case 'line-graph': return LineGraphControl;
     }
-    throw new Error('not supported');
+    console.warn(`${(param as any).type} has no associated UI component.`);
+    return void 0;
 }
 
 // type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean }
@@ -198,26 +204,47 @@ export class SelectControl extends SimpleParam<PD.Select<any>> {
 }
 
 export class IntervalControl extends SimpleParam<PD.Interval> {
-    // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
-    //     this.setState({ value: e.target.value });
-    //     this.props.onChange(e.target.value);
-    // }
-
+    onChange = (v: [number, number]) => { this.update(v); }
     renderControl() {
         return <span>interval TODO</span>;
     }
 }
 
+export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
+    onChange = (v: [number, number]) => { this.update(v); }
+    renderControl() {
+        return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!}
+            step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />;
+    }
+}
+
+let _colors: React.ReactFragment | undefined = void 0;
+function ColorOptions() {
+    if (_colors) return _colors;
+    _colors = <>{Object.keys(ColorNames).map(name =>
+        <option key={name} value={(ColorNames as { [k: string]: Color})[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color})[name])}` }} >
+            {name}
+        </option>
+    )}</>;
+    return _colors;
+}
+
+function ColorValueOption(color: Color) {
+    return !ColorNamesValueMap.has(color) ? <option key={Color.toHexString(color)} value={color} style={{ background: `${Color.toStyle(color)}` }} >
+        {Color.toHexString(color)}
+    </option> : null
+}
+
+
 export class ColorControl extends SimpleParam<PD.Color> {
     onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
         this.update(Color(parseInt(e.target.value)));
     }
 
     renderControl() {
-        return <select value={this.props.value} onChange={this.onChange}>
-            {Object.keys(ColorNames).map(name => {
-                return <option key={name} value={(ColorNames as { [k: string]: Color})[name]}>{name}</option>
-            })}
+        return <select value={this.props.value} onChange={this.onChange} style={{ borderLeft: `16px solid ${Color.toStyle(this.props.value)}` }}>
+            {ColorValueOption(this.props.value)}
+            {ColorOptions()}
         </select>;
     }
 }
@@ -233,6 +260,25 @@ export class Vec3Control extends SimpleParam<PD.Vec3> {
     }
 }
 
+export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> {
+    change(value: File) {
+        this.props.onChange({ name: this.props.name, param: this.props.param, value });
+    }
+
+    onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this.change(e.target.files![0]);
+    }
+
+    render() {
+        const value = this.props.value;
+
+        // return <input disabled={this.props.isDisabled} value={void 0} type='file' multiple={false} />
+        return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}>
+            {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} />
+        </div>
+    }
+}
+
 export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> {
     state = { isExpanded: false }
 
@@ -266,36 +312,41 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS
                 </div>
             </div>
             <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
-                {this.props.param.options.map(([value, label]) =>
-                    <div key={value} className='msp-row'>
+                {this.props.param.options.map(([value, label]) => {
+                    const sel = current.indexOf(value) >= 0;
+                    return <div key={value} className='msp-row'>
                         <button onClick={this.toggle(value)} disabled={this.props.isDisabled}>
-                            {current.indexOf(value) >= 0 ? `✓ ${label}` : `✗ ${label}`}
+                            <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span>
                         </button>
-                    </div>)}
+                </div> })}
             </div>
         </>;
     }
 }
 
 export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> {
-    state = { isExpanded: false }
+    state = { isExpanded: !!this.props.param.isExpanded }
 
-    change(value: PD.Mapped<any>['defaultValue'] ) {
+    change(value: any ) {
         this.props.onChange({ name: this.props.name, param: this.props.param, value });
     }
 
     onChangeParam: ParamOnChange = e => {
-        const value: PD.Mapped<any>['defaultValue'] = this.props.value;
-        this.change({ ...value.params, [e.name]: e.value });
+        this.change({ ...this.props.value, [e.name]: e.value });
     }
 
     toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
 
     render() {
-        const value: PD.Mapped<any>['defaultValue'] = this.props.value;
         const params = this.props.param.params;
         const label = this.props.param.label || camelCaseToWords(this.props.name);
 
+        const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />;
+
+        if (this.props.param.isFlat) {
+            return controls;
+        }
+
         return <div className='msp-control-group-wrapper'>
             <div className='msp-control-group-header'>
                 <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
@@ -304,7 +355,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>,
                 </button>
             </div>
             {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}>
-                <ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
+                {controls}
             </div>
             }
         </div>
@@ -322,8 +373,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
     }
 
     onChangeParam: ParamOnChange = e => {
-        const value: PD.Mapped<any>['defaultValue'] = this.props.value;
-        this.change({ name: value.name, params: e.value });
+        this.change({ name: this.props.value.name, params: e.value });
     }
 
     render() {
@@ -342,7 +392,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
 
         return <div>
             {select}
-            <Mapped param={param} value={value} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
+            <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />
         </div>
     }
 }

+ 53 - 5
src/mol-plugin/ui/controls/slider.tsx

@@ -38,12 +38,12 @@ export class Slider extends React.Component<{
     render() {
         let step = this.props.step;
         if (step === void 0) step = 1;
-        return  <div className='msp-slider'>
-        <div>
+        return <div className='msp-slider'>
             <div>
-            <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
-                onBeforeChange={this.begin}
-                onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
+                <div>
+                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                        onBeforeChange={this.begin}
+                        onChange={this.updateCurrent as any} onAfterChange={this.end as any} />
                 </div></div>
             <div>
                 {`${Math.round(100 * this.state.current) / 100}`}
@@ -52,6 +52,54 @@ export class Slider extends React.Component<{
     }
 }
 
+export class Slider2 extends React.Component<{
+    min: number,
+    max: number,
+    value: [number, number],
+    step?: number,
+    onChange: (v: [number, number]) => void,
+    disabled?: boolean
+}, { isChanging: boolean, current: [number, number] }> {
+
+    state = { isChanging: false, current: [0, 1] as [number, number] }
+
+    static getDerivedStateFromProps(props: { value: [number, number] }, state: { isChanging: boolean, current: [number, number] }) {
+        if (state.isChanging || (props.value[0] === state.current[0]) && (props.value[1] === state.current[1])) return null;
+        return { current: props.value };
+    }
+
+    begin = () => {
+        this.setState({ isChanging: true });
+    }
+
+    end = (v: [number, number]) => {
+        this.setState({ isChanging: false });
+        this.props.onChange(v);
+    }
+
+    updateCurrent = (current: [number, number]) => {
+        this.setState({ current });
+    }
+
+    render() {
+        let step = this.props.step;
+        if (step === void 0) step = 1;
+        return <div className='msp-slider2'>
+            <div>
+                {`${Math.round(100 * this.state.current[0]) / 100}`}
+            </div>
+            <div>
+                <div>
+                    <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled}
+                        onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} />
+                </div></div>
+            <div>
+                {`${Math.round(100 * this.state.current[1]) / 100}`}
+            </div>
+        </div>;
+    }
+}
+
 /**
  * The following code was adapted from react-components/slider library.
  * 

+ 18 - 16
src/mol-plugin/ui/plugin.tsx

@@ -19,6 +19,7 @@ import { BackgroundTaskProgress } from './task';
 import { ApplyActionContol } from './state/apply-action';
 import { PluginState } from 'mol-plugin/state';
 import { UpdateTransformContol } from './state/update-transform';
+import { StateObjectCell } from 'mol-state';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
 
@@ -81,13 +82,13 @@ export class State extends PluginComponent {
 
     render() {
         const kind = this.plugin.state.behavior.kind.value;
-        return <>
+        return <div className='msp-scrollable-container'>
             <div className='msp-btn-row-group msp-data-beh'>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button>
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button>
             </div>
             <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} />
-        </>
+        </div>
     }
 }
 
@@ -95,6 +96,7 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     private wrapper = React.createRef<HTMLDivElement>();
 
     componentDidMount() {
+        // TODO: only show last 100 entries.
         this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) }));
     }
 
@@ -110,10 +112,12 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> {
     }
 
     render() {
-        return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
-            <ul style={{ listStyle: 'none' }} className='msp-log-list'>
+        return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}>
+            <ul className='msp-list-unstyled'>
                 {this.state.entries.map((e, i) => <li key={i}>
-                    <b>[{formatTime(e!.timestamp)} | {e!.type}]</b> {e!.message}
+                    <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} />
+                    <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div>
+                    <div className='msp-log-entry'>{e!.message}</div>
                 </li>)}
             </ul>
         </div>;
@@ -141,22 +145,20 @@ export class CurrentObject extends PluginComponent {
         const current = this.current;
 
         const ref = current.ref;
-        // const n = this.props.plugin.state.data.tree.nodes.get(ref)!;
-        const obj = current.state.cells.get(ref)!;
-
-        const type = obj && obj.obj ? obj.obj.type : void 0;
+        const cell = current.state.cells.get(ref)!;
+        const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0;
 
-        const transform = current.state.transforms.get(ref);
+        const type = cell && cell.obj ? cell.obj.type : void 0;
+        const transform = cell.transform;
+        const def = transform.transformer.definition;
 
-        const actions = type
-            ? current.state.actions.fromType(type)
-            : []
+        const actions = type ? current.state.actions.fromType(type) : [];
         return <>
             <div className='msp-section-header'>
-                {obj.obj ? obj.obj.label : ref}
+                {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name}
             </div>
-            <UpdateTransformContol state={current.state} transform={transform} />
-            {
+            { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> }
+            {cell.status === 'ok' &&
                 actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />)
             }
         </>;

+ 14 - 2
src/mol-plugin/ui/state-tree.tsx

@@ -141,6 +141,18 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
         e.currentTarget.blur();
     }
 
+    highlight = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        e.currentTarget.blur();
+    }
+
+    clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        e.currentTarget.blur();
+    }
+
     render() {
         const n = this.props.state.transforms.get(this.props.nodeRef)!;
         const cell = this.props.state.cells.get(this.props.nodeRef)!;
@@ -150,7 +162,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
 
         let label: any;
         if (cell.status !== 'ok' || !cell.obj) {
-            const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name;
+            const name = n.transformer.definition.display.name;
             const title = `${cell.errorText}`
             label = <><b>{cell.status}</b> <a title={title} href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>;
         } else {
@@ -170,7 +182,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State
             <span className='msp-icon msp-icon-visual-visibility' />
         </button>;
 
-        return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`}>
+        return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
             {isCurrent ? <b>{label}</b> : label}
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />

+ 3 - 4
src/mol-plugin/ui/state.tsx

@@ -9,7 +9,6 @@ import * as React from 'react';
 import { PluginComponent } from './base';
 import { shallowEqual } from 'mol-util';
 import { List } from 'immutable';
-import { LogEntry } from 'mol-util/log-entry';
 import { ParameterControls } from './controls/parameters';
 import { ParamDefinition as PD} from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
@@ -58,7 +57,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC
         this.setState({ isUploading: true });
         await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl });
         this.setState({ isUploading: false });
-        this.plugin.log(LogEntry.message('Snapshot uploaded.'));
+        this.plugin.log.message('Snapshot uploaded.');
         UploadedEvent.next();
     }
 
@@ -128,7 +127,7 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
                 }))),
                 isFetching: false })
         } catch (e) {
-            this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e));
+            this.plugin.log.error('Fetching Remote Snapshots: ' + e);
             this.setState({ entries: List<RemoteEntry>(), isFetching: false })
         }
     }
@@ -149,7 +148,7 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e
 
     render() {
         return <div>
-            <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button>
+            <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control msp-section-header' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button>
 
             <ul style={{ listStyle: 'none' }} className='msp-state-list'>
                 {this.state.entries.valueSeq().map(e =><li key={e!.id}>

+ 2 - 1
src/mol-plugin/ui/state/apply-action.tsx

@@ -42,9 +42,10 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
     }
     getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); }
     getHeader() { return this.props.action.definition.display; }
-    getHeaderFallback() { return this.props.action.id; }
     canApply() { return !this.state.error && !this.state.busy; }
+    canAutoApply() { return false; }
     applyText() { return 'Apply'; }
+    isUpdate() { return false; }
 
     private _getInfo = memoizeOne((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 

+ 22 - 4
src/mol-plugin/ui/state/common.tsx

@@ -99,9 +99,10 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
     abstract applyAction(): Promise<void>;
     abstract getInfo(): StateTransformParameters.Props['info'];
     abstract getHeader(): Transformer.Definition['display'];
-    abstract getHeaderFallback(): string;
     abstract canApply(): boolean;
+    abstract canAutoApply(newParams: any): boolean;
     abstract applyText(): string;
+    abstract isUpdate(): boolean;
     abstract state: S;
 
     private busy: Subject<boolean>;
@@ -111,12 +112,29 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
         this.apply();
     }
 
+    private autoApplyHandle: number | undefined = void 0;
+    private clearAutoApply() {
+        if (this.autoApplyHandle !== void 0) {
+            clearTimeout(this.autoApplyHandle);
+            this.autoApplyHandle = void 0;
+        }
+    }
+
     events: StateTransformParameters.Props['events'] = {
         onEnter: this.onEnter,
-        onChange: (params, isInitial, errors) => this.setState({ params, isInitial, error: errors && errors[0] })
+        onChange: (params, isInitial, errors) => {
+            this.clearAutoApply();
+            this.setState({ params, isInitial, error: errors && errors[0] }, () => {
+                if (!isInitial && !this.state.error && this.canAutoApply(params)) {
+                    this.clearAutoApply();
+                    this.autoApplyHandle = setTimeout(this.apply, 50) as any as number;
+                }
+            });
+        }
     }
 
     apply = async () => {
+        this.clearAutoApply();
         this.setState({ busy: true });
         try {
             await this.applyAction();
@@ -146,13 +164,13 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState
 
     render() {
         const info = this.getInfo();
-        if (info.isEmpty) return null;
+        if (info.isEmpty && this.isUpdate()) return null;
 
         const display = this.getHeader();
 
         return <div className='msp-transform-wrapper'>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{(display && display.name) || this.getHeaderFallback()}</button>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button>
                 {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>}
             </div>
             {!this.state.isCollapsed && <>

+ 13 - 1
src/mol-plugin/ui/state/update-transform.tsx

@@ -29,9 +29,21 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr
     applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); }
     getInfo() { return this._getInfo(this.props.transform); }
     getHeader() { return this.props.transform.transformer.definition.display; }
-    getHeaderFallback() { return this.props.transform.transformer.definition.name; }
     canApply() { return !this.state.error && !this.state.busy && !this.state.isInitial; }
     applyText() { return this.canApply() ? 'Update' : 'Nothing to Update'; }
+    isUpdate() { return true; }
+
+    canAutoApply(newParams: any) {
+        const autoUpdate = this.props.transform.transformer.definition.canAutoUpdate
+        if (!autoUpdate) return false;
+
+        const { state } = this.props;
+        const cell = state.cells.get(this.props.transform.ref);
+        if (!cell || !cell.sourceRef || cell.status !== 'ok') return false;
+        const parentCell = state.cells.get(cell.sourceRef)!;
+
+        return autoUpdate({ a: cell.obj!, b: parentCell.obj!, oldParams: this.props.transform.params, newParams }, this.plugin);
+    }
 
     private _getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform));
 

+ 65 - 0
src/mol-plugin/util/custom-prop-registry.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ModelPropertyDescriptor, Model } from 'mol-model/structure';
+import { OrderedMap } from 'immutable';
+import { ParamDefinition } from 'mol-util/param-definition';
+import { Task } from 'mol-task';
+
+export { CustomPropertyRegistry }
+
+class CustomPropertyRegistry {
+    private providers = OrderedMap<string, CustomPropertyRegistry.Provider>().asMutable();
+
+    getSelect(model: Model) {
+        const values = this.providers.values();
+        const options: [string, string][] = [], selected: string[] = [];
+        while (true) {
+            const v = values.next();
+            if (v.done) break;
+            if (!v.value.attachableTo(model)) continue;
+            options.push(v.value.option);
+            if (v.value.defaultSelected) selected.push(v.value.option[0]);
+        }
+        return ParamDefinition.MultiSelect(selected, options);
+    }
+
+    getDefault(model: Model) {
+        const values = this.providers.values();
+        const selected: string[] = [];
+        while (true) {
+            const v = values.next();
+            if (v.done) break;
+            if (!v.value.attachableTo(model)) continue;
+            if (v.value.defaultSelected) selected.push(v.value.option[0]);
+        }
+        return selected;
+    }
+
+    get(name: string) {
+        const prop = this.providers.get(name);
+        if (!prop) throw new Error(`Custom prop '${name}' is not registered.`);
+        return this.providers.get(name);
+    }
+
+    register(provider: CustomPropertyRegistry.Provider) {
+        this.providers.set(provider.descriptor.name, provider);
+    }
+
+    unregister(name: string) {
+        this.providers.delete(name);
+    }
+}
+
+namespace CustomPropertyRegistry {
+    export interface Provider {
+        option: [string, string],
+        defaultSelected: boolean,
+        descriptor: ModelPropertyDescriptor<any, any>,
+        attachableTo: (model: Model) => boolean,
+        attach: (model: Model) => Task<boolean>
+    }
+}

+ 52 - 26
src/mol-repr/representation.ts

@@ -14,23 +14,31 @@ import { WebGLContext } from 'mol-gl/webgl/context';
 import { getQualityProps } from './util';
 import { ColorTheme } from 'mol-theme/color';
 import { SizeTheme } from 'mol-theme/size';
-import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
+import { Theme, ThemeRegistryContext, createEmptyTheme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
+import { Mat4 } from 'mol-math/linear-algebra';
 
 // export interface RepresentationProps {
 //     visuals?: string[]
 // }
 export type RepresentationProps = { [k: string]: any }
 
+export interface RepresentationContext {
+    readonly webgl?: WebGLContext
+    readonly colorThemeRegistry: ColorTheme.Registry
+    readonly sizeThemeRegistry: SizeTheme.Registry
+}
+
 export type RepresentationParamsGetter<D, P extends PD.Params> = (ctx: ThemeRegistryContext, data: D) => P
+export type RepresentationFactory<D, P extends PD.Params> = (ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>) => Representation<D, P>
 
 //
 
 export interface RepresentationProvider<D, P extends PD.Params> {
     readonly label: string
     readonly description: string
-    readonly factory: (getParams: RepresentationParamsGetter<D, P>) => Representation<D, P>
-    readonly getParams: (ctx: ThemeRegistryContext, data: D) => P
+    readonly factory: RepresentationFactory<D, P>
+    readonly getParams: RepresentationParamsGetter<D, P>
     readonly defaultValues: PD.Values<P>
 }
 
@@ -71,12 +79,6 @@ export class RepresentationRegistry<D> {
 
 //
 
-export interface RepresentationContext {
-    webgl?: WebGLContext
-    colorThemeRegistry: ColorTheme.Registry
-    sizeThemeRegistry: SizeTheme.Registry
-}
-
 export { Representation }
 interface Representation<D, P extends PD.Params = {}> {
     readonly label: string
@@ -86,30 +88,50 @@ interface Representation<D, P extends PD.Params = {}> {
     readonly renderObjects: ReadonlyArray<RenderObject>
     readonly props: Readonly<PD.Values<P>>
     readonly params: Readonly<P>
-    createOrUpdate: (ctx: RepresentationContext, props?: Partial<PD.Values<P>>, data?: D) => Task<void>
+    readonly state: Readonly<Representation.State>
+    readonly theme: Readonly<Theme>
+    createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
+    setState: (state: Partial<Representation.State>) => void
+    setTheme: (theme: Theme) => void
     getLoci: (pickingId: PickingId) => Loci
     mark: (loci: Loci, action: MarkerAction) => boolean
-    setVisibility: (value: boolean) => void
-    setPickable: (value: boolean) => void
     destroy: () => void
 }
 namespace Representation {
+    export interface State {
+        visible: boolean
+        pickable: boolean
+        syncManually: boolean
+        transform: Mat4
+    }
+    export function createState() {
+        return { visible: false, pickable: false, syncManually: false, transform: Mat4.identity() }
+    }
+    export function updateState(state: State, update: Partial<State>) {
+        if (update.visible !== undefined) state.visible = update.visible
+        if (update.pickable !== undefined) state.pickable = update.pickable
+        if (update.syncManually !== undefined) state.syncManually = update.syncManually
+        if (update.transform !== undefined) Mat4.copy(state.transform, update.transform)
+    }
+
     export type Any = Representation<any>
     export const Empty: Any = {
-        label: '', groupCount: 0, renderObjects: [], props: {}, params: {}, updated: new Subject(),
+        label: '', groupCount: 0, renderObjects: [], props: {}, params: {}, updated: new Subject(), state: createState(), theme: createEmptyTheme(),
         createOrUpdate: () => Task.constant('', undefined),
+        setState: () => {},
+        setTheme: () => {},
         getLoci: () => EmptyLoci,
         mark: () => false,
-        setVisibility: () => {},
-        setPickable: () => {},
         destroy: () => {}
     }
 
-    export type Def<D, P extends PD.Params = {}> = { [k: string]: (getParams: RepresentationParamsGetter<D, P>) => Representation<any, P> }
+    export type Def<D, P extends PD.Params = {}> = { [k: string]: RepresentationFactory<D, P> }
 
-    export function createMulti<D, P extends PD.Params = {}>(label: string, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> {
+    export function createMulti<D, P extends PD.Params = {}>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> {
         let version = 0
         const updated = new Subject<number>()
+        const currentState = Representation.createState()
+        let currentTheme = createEmptyTheme()
 
         let currentParams: P
         let currentProps: PD.Values<P>
@@ -118,7 +140,7 @@ namespace Representation {
         const reprMap: { [k: number]: string } = {}
         const reprList: Representation<D, P>[] = Object.keys(reprDefs).map((name, i) => {
             reprMap[i] = name
-            return reprDefs[name](getParams)
+            return reprDefs[name](ctx, getParams)
         })
 
         return {
@@ -154,7 +176,7 @@ namespace Representation {
                 return props as P
             },
             get params() { return currentParams },
-            createOrUpdate: (ctx: RepresentationContext, props: Partial<P> = {}, data?: D) => {
+            createOrUpdate: (props: Partial<P> = {}, data?: D) => {
                 if (data && data !== currentData) {
                     currentParams = getParams(ctx, data)
                     currentData = data
@@ -167,12 +189,14 @@ namespace Representation {
                 return Task.create(`Creating '${label}' representation`, async runtime => {
                     for (let i = 0, il = reprList.length; i < il; ++i) {
                         if (!visuals || visuals.includes(reprMap[i])) {
-                            await reprList[i].createOrUpdate(ctx, currentProps, currentData).runInContext(runtime)
+                            await reprList[i].createOrUpdate(currentProps, currentData).runInContext(runtime)
                         }
                     }
                     updated.next(version++)
                 })
             },
+            get state() { return currentState },
+            get theme() { return currentTheme },
             getLoci: (pickingId: PickingId) => {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     const loci = reprList[i].getLoci(pickingId)
@@ -187,14 +211,15 @@ namespace Representation {
                 }
                 return marked
             },
-            setVisibility: (value: boolean) => {
+            setState: (state: Partial<State>) => {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
-                    reprList[i].setVisibility(value)
+                    reprList[i].setState(state)
                 }
+                Representation.updateState(currentState, state)
             },
-            setPickable: (value: boolean) => {
+            setTheme: (theme: Theme) => {
                 for (let i = 0, il = reprList.length; i < il; ++i) {
-                    reprList[i].setPickable(value)
+                    reprList[i].setTheme(theme)
                 }
             },
             destroy() {
@@ -209,9 +234,10 @@ namespace Representation {
 //
 
 export interface VisualContext {
-    webgl?: WebGLContext
-    runtime: RuntimeContext,
+    readonly runtime: RuntimeContext
+    readonly webgl?: WebGLContext
 }
+// export type VisualFactory<D, P extends PD.Params> = (ctx: VisualContext) => Visual<D, P>
 
 export interface Visual<D, P extends PD.Params> {
     /** Number of addressable groups in all instances of the visual */

+ 17 - 11
src/mol-repr/shape/representation.ts

@@ -18,7 +18,7 @@ import { createRenderableState } from 'mol-geo/geometry/geometry';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data';
 import { LocationIterator } from 'mol-geo/util/location-iterator';
-import { createTheme } from 'mol-theme/theme';
+import { createEmptyTheme, Theme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
 
 export interface ShapeRepresentation<P extends ShapeParams> extends Representation<Shape, P> { }
@@ -30,17 +30,19 @@ export const ShapeParams = {
 }
 export type ShapeParams = typeof ShapeParams
 
-export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentation<P> {
+export function ShapeRepresentation<P extends ShapeParams>(ctx: RepresentationContext): ShapeRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const _state = Representation.createState()
     const renderObjects: RenderObject[] = []
     let _renderObject: MeshRenderObject | undefined
     let _shape: Shape
+    let _theme = createEmptyTheme()
     let currentProps: PD.Values<P> = PD.getDefaultValues(ShapeParams) as PD.Values<P>
     let currentParams: P
     let locationIt: LocationIterator
 
-    function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, shape?: Shape) {
+    function createOrUpdate(props: Partial<PD.Values<P>> = {}, shape?: Shape) {
         currentProps = Object.assign({}, currentProps, props)
         if (shape) _shape = shape
 
@@ -51,10 +53,9 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio
 
             const mesh = _shape.mesh
             locationIt = ShapeGroupIterator.fromShape(_shape)
-            const theme = createTheme(ctx, currentProps, {})
             const transform = createIdentityTransform()
 
-            const values = await Mesh.createValues(runtime, mesh, transform, locationIt, theme, currentProps)
+            const values = await Mesh.createValues(runtime, mesh, transform, locationIt, _theme, currentProps)
             const state = createRenderableState(currentProps)
 
             _renderObject = createMeshRenderObject(values, state)
@@ -65,11 +66,13 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio
 
     return {
         label: 'Shape mesh',
-        updated,
         get groupCount () { return locationIt ? locationIt.count : 0 },
         get renderObjects () { return renderObjects },
-        get params () { return currentParams },
         get props () { return currentProps },
+        get params () { return currentParams },
+        get state() { return _state },
+        get theme() { return _theme },
+        updated,
         createOrUpdate,
         getLoci(pickingId: PickingId) {
             const { objectId, groupId } = pickingId
@@ -103,11 +106,14 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio
             }
             return changed
         },
-        setVisibility(value: boolean) {
-            renderObjects.forEach(ro => ro.state.visible = value)
+        setState(state: Partial<Representation.State>) {
+            if (state.visible !== undefined) renderObjects.forEach(ro => ro.state.visible = state.visible!)
+            if (state.pickable !== undefined) renderObjects.forEach(ro => ro.state.pickable = state.pickable!)
+
+            Representation.updateState(_state, state)
         },
-        setPickable(value: boolean) {
-            renderObjects.forEach(ro => ro.state.pickable = value)
+        setTheme(theme: Theme) {
+            _theme = theme
         },
         destroy() {
             // TODO

+ 19 - 14
src/mol-repr/structure/complex-representation.ts

@@ -12,33 +12,33 @@ import { StructureRepresentation, StructureParams } from './representation';
 import { ComplexVisual } from './complex-visual';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
-import { Theme, createTheme } from 'mol-theme/theme';
+import { RepresentationContext, RepresentationParamsGetter, Representation } from 'mol-repr/representation';
+import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { Subject } from 'rxjs';
 
-export function ComplexRepresentation<P extends StructureParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
+export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const _state = Representation.createState()
     let visual: ComplexVisual<P> | undefined
 
     let _structure: Structure
     let _params: P
     let _props: PD.Values<P>
-    let _theme: Theme
+    let _theme = createEmptyTheme()
 
-    function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, structure?: Structure) {
+    function createOrUpdate(props: Partial<PD.Values<P>> = {}, structure?: Structure) {
         if (structure && structure !== _structure) {
             _params = getParams(ctx, structure)
             _structure = structure
             if (!_props) _props = PD.getDefaultValues(_params)
         }
         _props = Object.assign({}, _props, props)
-        _theme = createTheme(ctx, { structure: _structure }, props, _theme)
 
         return Task.create('Creating or updating ComplexRepresentation', async runtime => {
             if (!visual) visual = visualCtor()
-            await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, structure)
+            await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure)
             updated.next(version++)
         });
     }
@@ -51,12 +51,15 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         return visual ? visual.mark(loci, action) : false
     }
 
-    function setVisibility(value: boolean) {
-        if (visual) visual.setVisibility(value)
+    function setState(state: Partial<Representation.State>) {
+        if (state.visible !== undefined && visual) visual.setVisibility(state.visible)
+        if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
+
+        Representation.updateState(_state, state)
     }
 
-    function setPickable(value: boolean) {
-        if (visual) visual.setPickable(value)
+    function setTheme(theme: Theme) {
+        _theme = theme
     }
 
     function destroy() {
@@ -73,12 +76,14 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         },
         get props() { return _props },
         get params() { return _params },
-        get updated() { return updated },
+        get state() { return _state },
+        get theme() { return _theme },
+        updated,
         createOrUpdate,
+        setState,
+        setTheme,
         getLoci,
         mark,
-        setVisibility,
-        setPickable,
         destroy
     }
 }

+ 11 - 8
src/mol-repr/structure/complex-visual.ts

@@ -48,12 +48,13 @@ interface ComplexVisualBuilder<P extends ComplexParams, G extends Geometry> {
 interface ComplexVisualGeometryBuilder<P extends ComplexParams, G extends Geometry> extends ComplexVisualBuilder<P, G> {
     createEmptyGeometry(geometry?: G): G
     createRenderObject(ctx: VisualContext, structure: Structure, geometry: Geometry, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<ComplexRenderObject>
-    updateValues(values: RenderableValues, newProps: PD.Values<P>): void
+    updateValues(values: RenderableValues, newProps: PD.Values<P>): void,
+    updateBoundingSphere(values: RenderableValues, geometry: Geometry): void
 }
 
 export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeometryBuilder<P, Geometry>): ComplexVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
-    const { createRenderObject, updateValues } = builder
+    const { createRenderObject, updateValues, updateBoundingSphere } = builder
     const updateState = VisualUpdateState.create()
 
     let renderObject: ComplexRenderObject | undefined
@@ -85,20 +86,21 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
         VisualUpdateState.reset(updateState)
         setUpdateState(updateState, newProps, currentProps, theme, currentTheme)
 
+        if (!ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true
+        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
+
         const newConformationHash = Structure.conformationHash(currentStructure)
         if (newConformationHash !== conformationHash) {
             conformationHash = newConformationHash
             updateState.createGeometry = true
         }
 
-        if (ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true
-        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
-
         //
 
         if (updateState.createGeometry) {
             geometry = await createGeometry(ctx, currentStructure, theme, newProps, geometry)
             ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
+            updateBoundingSphere(renderObject.values, geometry)
             updateState.updateColor = true
         }
 
@@ -129,7 +131,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
                 throw new Error('missing structure')
             } else if (structure && (!currentStructure || !renderObject)) {
                 await create(ctx, structure, theme, props)
-            } else if (structure && structure.hashCode !== currentStructure.hashCode) {
+            } else if (structure && !Structure.areEquivalent(structure, currentStructure)) {
                 await create(ctx, structure, theme, props)
             } else {
                 if (structure && Structure.conformationHash(structure) !== Structure.conformationHash(currentStructure)) {
@@ -153,7 +155,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo
             }
 
             let changed = false
-            if (isEveryLoci(loci)) {
+            if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructure)) {
                 changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, currentStructure, apply)
@@ -195,6 +197,7 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM
         },
         createEmptyGeometry: Mesh.createEmpty,
         createRenderObject: createComplexMeshRenderObject,
-        updateValues: Mesh.updateValues
+        updateValues: Mesh.updateValues,
+        updateBoundingSphere: Mesh.updateBoundingSphere
     })
 }

+ 6 - 8
src/mol-repr/structure/representation/ball-and-stick.ts

@@ -11,16 +11,15 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
 import { ComplexRepresentation } from '../complex-representation';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
-import { Representation, RepresentationParamsGetter } from 'mol-repr/representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
-import { BuiltInColorThemeOptions, BuiltInColorThemes, ColorTheme } from 'mol-theme/color';
 import { UnitKind, UnitKindOptions } from '../visual/util/common';
 
 const BallAndStickVisuals = {
-    'element-sphere': (getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', getParams, ElementSphereVisual),
-    'intra-link': (getParams: RepresentationParamsGetter<Structure, IntraUnitLinkParams>) => UnitsRepresentation('Intra-unit link cylinder', getParams, IntraUnitLinkVisual),
-    'inter-link': (getParams: RepresentationParamsGetter<Structure, InterUnitLinkParams>) => ComplexRepresentation('Inter-unit link cylinder', getParams, InterUnitLinkVisual),
+    'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', ctx, getParams, ElementSphereVisual),
+    'intra-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitLinkParams>) => UnitsRepresentation('Intra-unit link cylinder', ctx, getParams, IntraUnitLinkVisual),
+    'inter-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitLinkParams>) => ComplexRepresentation('Inter-unit link cylinder', ctx, getParams, InterUnitLinkVisual),
 }
 type BallAndStickVisualName = keyof typeof BallAndStickVisuals
 const BallAndStickVisualOptions = Object.keys(BallAndStickVisuals).map(name => [name, name] as [BallAndStickVisualName, string])
@@ -32,7 +31,6 @@ export const BallAndStickParams = {
     unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions),
     sizeFactor: PD.Numeric(0.3, { min: 0.01, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2/3, { min: 0.01, max: 3, step: 0.01 }),
-    colorTheme: PD.Mapped('element-symbol', BuiltInColorThemeOptions, name => PD.Group((BuiltInColorThemes as { [k: string]: ColorTheme.Provider<any> })[name].getParams({}))),
     visuals: PD.MultiSelect<BallAndStickVisualName>(['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions),
 }
 export type BallAndStickParams = typeof BallAndStickParams
@@ -41,8 +39,8 @@ export function getBallAndStickParams(ctx: ThemeRegistryContext, structure: Stru
 }
 
 export type BallAndStickRepresentation = StructureRepresentation<BallAndStickParams>
-export function BallAndStickRepresentation(getParams: RepresentationParamsGetter<Structure, BallAndStickParams>): BallAndStickRepresentation {
-    return Representation.createMulti('Ball & Stick', getParams, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>)
+export function BallAndStickRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BallAndStickParams>): BallAndStickRepresentation {
+    return Representation.createMulti('Ball & Stick', ctx, getParams, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>)
 }
 
 export const BallAndStickRepresentationProvider: StructureRepresentationProvider<typeof BallAndStickParams> = {

+ 6 - 9
src/mol-repr/structure/representation/carbohydrate.ts

@@ -10,15 +10,14 @@ import { CarbohydrateTerminalLinkParams, CarbohydrateTerminalLinkVisual } from '
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ComplexRepresentation } from '../complex-representation';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
-import { Representation, RepresentationParamsGetter } from 'mol-repr/representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
-import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color';
 
 const CarbohydrateVisuals = {
-    'carbohydrate-symbol': (getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', getParams, CarbohydrateSymbolVisual),
-    'carbohydrate-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', getParams, CarbohydrateLinkVisual),
-    'carbohydrate-terminal-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', getParams, CarbohydrateTerminalLinkVisual),
+    'carbohydrate-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', ctx, getParams, CarbohydrateSymbolVisual),
+    'carbohydrate-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', ctx, getParams, CarbohydrateLinkVisual),
+    'carbohydrate-terminal-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', ctx, getParams, CarbohydrateTerminalLinkVisual),
 }
 type CarbohydrateVisualName = keyof typeof CarbohydrateVisuals
 const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [name, name] as [CarbohydrateVisualName, string])
@@ -27,18 +26,16 @@ export const CarbohydrateParams = {
     ...CarbohydrateSymbolParams,
     ...CarbohydrateLinkParams,
     ...CarbohydrateTerminalLinkParams,
-    colorTheme: PD.Mapped('carbohydrate-symbol', BuiltInColorThemeOptions, getBuiltInColorThemeParams),
     visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link', 'carbohydrate-terminal-link'], CarbohydrateVisualOptions),
 }
-PD.getDefaultValues(CarbohydrateParams).colorTheme.name
 export type CarbohydrateParams = typeof CarbohydrateParams
 export function getCarbohydrateParams(ctx: ThemeRegistryContext, structure: Structure) {
     return PD.clone(CarbohydrateParams)
 }
 
 export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateParams>
-export function CarbohydrateRepresentation(getParams: RepresentationParamsGetter<Structure, CarbohydrateParams>): CarbohydrateRepresentation {
-    return Representation.createMulti('Carbohydrate', getParams, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>)
+export function CarbohydrateRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateParams>): CarbohydrateRepresentation {
+    return Representation.createMulti('Carbohydrate', ctx, getParams, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>)
 }
 
 export const CarbohydrateRepresentationProvider: StructureRepresentationProvider<CarbohydrateParams> = {

+ 8 - 11
src/mol-repr/structure/representation/cartoon.ts

@@ -10,17 +10,16 @@ import { NucleotideBlockVisual, NucleotideBlockParams } from '../visual/nucleoti
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
-import { Representation, RepresentationParamsGetter } from 'mol-repr/representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { PolymerDirectionVisual, PolymerDirectionParams } from '../visual/polymer-direction-wedge';
 import { Structure } from 'mol-model/structure';
 import { ThemeRegistryContext } from 'mol-theme/theme';
-import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color';
 
 const CartoonVisuals = {
-    'polymer-trace': (getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', getParams, PolymerTraceVisual),
-    'polymer-gap': (getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', getParams, PolymerGapVisual),
-    'nucleotide-block': (getParams: RepresentationParamsGetter<Structure, NucleotideBlockParams>) => UnitsRepresentation('Nucleotide block mesh', getParams, NucleotideBlockVisual),
-    'direction-wedge': (getParams: RepresentationParamsGetter<Structure, PolymerDirectionParams>) => UnitsRepresentation('Polymer direction wedge', getParams, PolymerDirectionVisual)
+    'polymer-trace': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', ctx, getParams, PolymerTraceVisual),
+    'polymer-gap': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', ctx, getParams, PolymerGapVisual),
+    'nucleotide-block': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NucleotideBlockParams>) => UnitsRepresentation('Nucleotide block mesh', ctx, getParams, NucleotideBlockVisual),
+    'direction-wedge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerDirectionParams>) => UnitsRepresentation('Polymer direction wedge', ctx, getParams, PolymerDirectionVisual)
 }
 type CartoonVisualName = keyof typeof CartoonVisuals
 const CartoonVisualOptions = Object.keys(CartoonVisuals).map(name => [name, name] as [CartoonVisualName, string])
@@ -31,23 +30,21 @@ export const CartoonParams = {
     ...NucleotideBlockParams,
     ...PolymerDirectionParams,
     sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
-    colorTheme: PD.Mapped('polymer-index', BuiltInColorThemeOptions, getBuiltInColorThemeParams),
     visuals: PD.MultiSelect<CartoonVisualName>(['polymer-trace', 'polymer-gap', 'nucleotide-block'], CartoonVisualOptions),
 }
-PD.getDefaultValues(CartoonParams).colorTheme.name
 export type CartoonParams = typeof CartoonParams
 export function getCartoonParams(ctx: ThemeRegistryContext, structure: Structure) {
     return PD.clone(CartoonParams)
 }
 
 export type CartoonRepresentation = StructureRepresentation<CartoonParams>
-export function CartoonRepresentation(getParams: RepresentationParamsGetter<Structure, CartoonParams>): CartoonRepresentation {
-    return Representation.createMulti('Cartoon', getParams, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>)
+export function CartoonRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CartoonParams>): CartoonRepresentation {
+    return Representation.createMulti('Cartoon', ctx, getParams, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>)
 }
 
 export const CartoonRepresentationProvider: StructureRepresentationProvider<CartoonParams> = {
     label: 'Cartoon',
-    description: 'Displays a ribbon smoothly following the trace atom of polymers.',
+    description: 'Displays a ribbon smoothly following the trace atoms of polymers.',
     factory: CartoonRepresentation,
     getParams: getCartoonParams,
     defaultValues: PD.getDefaultValues(CartoonParams)

+ 7 - 32
src/mol-repr/structure/representation/molecular-surface.ts

@@ -10,15 +10,14 @@ import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaus
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { GaussianDensityVolumeParams, GaussianDensityVolumeVisual } from '../visual/gaussian-density-volume';
 import { StructureRepresentation, StructureRepresentationProvider } from '../representation';
-import { Representation, RepresentationParamsGetter } from 'mol-repr/representation';
+import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation';
 import { ThemeRegistryContext } from 'mol-theme/theme';
 import { Structure } from 'mol-model/structure';
-import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color';
 
 const MolecularSurfaceVisuals = {
-    'gaussian-surface': (getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', getParams, GaussianSurfaceVisual),
-    'gaussian-wireframe': (getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', getParams, GaussianWireframeVisual),
-    'gaussian-volume': (getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => UnitsRepresentation('Gaussian volume', getParams, GaussianDensityVolumeVisual)
+    'gaussian-surface': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceVisual),
+    'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual),
+    'gaussian-volume': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => UnitsRepresentation('Gaussian volume', ctx, getParams, GaussianDensityVolumeVisual)
 }
 type MolecularSurfaceVisualName = keyof typeof MolecularSurfaceVisuals
 const MolecularSurfaceVisualOptions = Object.keys(MolecularSurfaceVisuals).map(name => [name, name] as [MolecularSurfaceVisualName, string])
@@ -27,18 +26,16 @@ export const MolecularSurfaceParams = {
     ...GaussianSurfaceParams,
     ...GaussianWireframeParams,
     ...GaussianDensityVolumeParams,
-    colorTheme: PD.Mapped('polymer-index', BuiltInColorThemeOptions, getBuiltInColorThemeParams),
     visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['gaussian-surface'], MolecularSurfaceVisualOptions),
 }
-PD.getDefaultValues(MolecularSurfaceParams).colorTheme.name
 export type MolecularSurfaceParams = typeof MolecularSurfaceParams
 export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) {
     return PD.clone(MolecularSurfaceParams)
 }
 
 export type MolecularSurfaceRepresentation = StructureRepresentation<MolecularSurfaceParams>
-export function MolecularSurfaceRepresentation(getParams: RepresentationParamsGetter<Structure, MolecularSurfaceParams>): MolecularSurfaceRepresentation {
-    return Representation.createMulti('Molecular Surface', getParams, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>)
+export function MolecularSurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularSurfaceParams>): MolecularSurfaceRepresentation {
+    return Representation.createMulti('Molecular Surface', ctx, getParams, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>)
 }
 
 export const MolecularSurfaceRepresentationProvider: StructureRepresentationProvider<MolecularSurfaceParams> = {
@@ -47,26 +44,4 @@ export const MolecularSurfaceRepresentationProvider: StructureRepresentationProv
     factory: MolecularSurfaceRepresentation,
     getParams: getMolecularSurfaceParams,
     defaultValues: PD.getDefaultValues(MolecularSurfaceParams)
-}
-
-
-
-// export const MolecularSurfaceParams = {
-//     ...GaussianSurfaceParams,
-//     ...GaussianWireframeParams,
-//     ...GaussianDensityVolumeParams,
-// }
-// export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) {
-//     return MolecularSurfaceParams // TODO return copy
-// }
-// export type MolecularSurfaceProps = PD.DefaultValues<typeof MolecularSurfaceParams>
-
-// export type MolecularSurfaceRepresentation = StructureRepresentation<MolecularSurfaceProps>
-
-// export function MolecularSurfaceRepresentation(defaultProps: MolecularSurfaceProps): MolecularSurfaceRepresentation {
-//     return Representation.createMulti('Molecular Surface', defaultProps, [
-//         UnitsRepresentation('Gaussian surface', defaultProps, GaussianSurfaceVisual),
-//         UnitsRepresentation('Gaussian wireframe', defaultProps, GaussianWireframeVisual),
-//         UnitsRepresentation('Gaussian volume', defaultProps, GaussianDensityVolumeVisual)
-//     ])
-// }
+}

+ 38 - 28
src/mol-repr/structure/units-representation.ts

@@ -8,13 +8,13 @@
 import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { RenderObject } from 'mol-gl/render-object';
-import { Visual, RepresentationContext, RepresentationParamsGetter } from '../representation';
+import { Visual, RepresentationContext, RepresentationParamsGetter, Representation } from '../representation';
 import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
 import { StructureGroup } from './units-visual';
 import { StructureRepresentation, StructureParams } from './representation';
 import { PickingId } from 'mol-geo/geometry/picking';
 import { MarkerAction } from 'mol-geo/geometry/marker-data';
-import { Theme, createTheme } from 'mol-theme/theme';
+import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { UnitKind, UnitKindOptions } from './visual/util/common';
 import { Subject } from 'rxjs';
@@ -27,40 +27,40 @@ export type UnitsParams = typeof UnitsParams
 
 export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGroup, P> { }
 
-export function UnitsRepresentation<P extends UnitsParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
+export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const _state = Representation.createState()
     let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
 
     let _structure: Structure
     let _groups: ReadonlyArray<Unit.SymmetryGroup>
     let _params: P
     let _props: PD.Values<P>
-    let _theme: Theme
+    let _theme = createEmptyTheme()
 
-    function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, structure?: Structure) {
+    function createOrUpdate(props: Partial<PD.Values<P>> = {}, structure?: Structure) {
         if (structure && structure !== _structure) {
             _params = getParams(ctx, structure)
             if (!_props) _props = PD.getDefaultValues(_params)
         }
         _props = Object.assign({}, _props, props)
-        _theme = createTheme(ctx, { structure: structure || _structure }, props, _theme)
 
         return Task.create('Creating or updating UnitsRepresentation', async runtime => {
             if (!_structure && !structure) {
                 throw new Error('missing structure')
             } else if (structure && !_structure) {
-                // console.log('initial structure')
+                // console.log(label, '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.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure })
+                    await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                     visuals.set(group.hashCode, { visual, group })
                 }
-            } else if (structure && _structure.hashCode !== structure.hashCode) {
-                // console.log('_structure.hashCode !== structure.hashCode')
+            } else if (structure && !Structure.areEquivalent(structure, _structure)) {
+                // console.log(label, 'structure not equivalent')
                 // 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;
@@ -71,18 +71,25 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
                     const group = _groups[i];
                     const visualGroup = oldVisuals.get(group.hashCode)
                     if (visualGroup) {
+                        // console.log(label, 'found visualGroup to reuse')
+                        // console.log('old', visualGroup.group)
+                        // console.log('new', group)
                         const { visual } = visualGroup
-                        await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure })
+                        await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                         visuals.set(group.hashCode, { visual, group })
                         oldVisuals.delete(group.hashCode)
                     } else {
+                        // console.log(label, 'not found visualGroup to reuse, creating new')
                         // newGroups.push(group)
                         const visual = visualCtor()
-                        await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure })
+                        await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                         visuals.set(group.hashCode, { visual, group })
                     }
                 }
-                oldVisuals.forEach(({ visual }) => visual.destroy())
+                oldVisuals.forEach(({ visual }) => {
+                    // console.log(label, 'removed unused visual')
+                    visual.destroy()
+                })
 
                 // TODO review logic
                 // For new groups, re-use left-over visuals
@@ -94,30 +101,32 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
                 //     visuals.set(group.hashCode, { visual, group })
                 // })
                 // unusedVisuals.forEach(visual => visual.destroy())
-            } else if (structure && structure !== _structure && _structure.hashCode === structure.hashCode) {
-                // console.log('_structure.hashCode === structure.hashCode')
+            } else if (structure && structure !== _structure && Structure.areEquivalent(structure, _structure)) {
+                // console.log(label, 'structures equivalent but not identical')
                 // 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;
+                // console.log('new', structure.unitSymmetryGroups)
+                // console.log('old', _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, runtime }, _theme, _props, { group, structure })
+                        await visualGroup.visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure })
                         visualGroup.group = group
                     } else {
                         throw new Error(`expected to find visual for hashCode ${group.hashCode}`)
                     }
                 }
             } else {
-                // console.log('no new structure')
+                // console.log(label, 'no new structure')
                 // No new structure given, just update all visuals with new props.
                 const visualsList: [ UnitsVisual<P>, Unit.SymmetryGroup ][] = [] // TODO avoid allocation
                 visuals.forEach(({ visual, group }) => visualsList.push([ visual, group ]))
                 for (let i = 0, il = visualsList.length; i < il; ++i) {
                     const [ visual ] = visualsList[i]
-                    await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props)
+                    await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props)
                 }
             }
             if (structure) _structure = structure
@@ -142,16 +151,15 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
         return changed
     }
 
-    function setVisibility(value: boolean) {
-        visuals.forEach(({ visual }) => {
-            visual.setVisibility(value)
-        })
+    function setState(state: Partial<Representation.State>) {
+        if (state.visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(state.visible!))
+        if (state.pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(state.pickable!))
+
+        Representation.updateState(_state, state)
     }
 
-    function setPickable(value: boolean) {
-        visuals.forEach(({ visual }) => {
-            visual.setPickable(value)
-        })
+    function setTheme(theme: Theme) {
+        _theme = theme
     }
 
     function destroy() {
@@ -177,12 +185,14 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar
         },
         get props() { return _props },
         get params() { return _params },
+        get state() { return _state },
+        get theme() { return _theme },
         updated,
         createOrUpdate,
+        setState,
+        setTheme,
         getLoci,
         mark,
-        setVisibility,
-        setPickable,
         destroy
     }
 }

+ 47 - 27
src/mol-repr/structure/units-visual.ts

@@ -34,13 +34,6 @@ export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
 
 export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { }
 
-function sameGroupConformation(groupA: Unit.SymmetryGroup, groupB: Unit.SymmetryGroup) {
-    return (
-        groupA.units.length === groupB.units.length &&
-        Unit.conformationId(groupA.units[0]) === Unit.conformationId(groupB.units[0])
-    )
-}
-
 type UnitsRenderObject = MeshRenderObject | LinesRenderObject | PointsRenderObject | DirectVolumeRenderObject
 
 interface UnitsVisualBuilder<P extends UnitsParams, G extends Geometry> {
@@ -56,11 +49,12 @@ interface UnitsVisualGeometryBuilder<P extends UnitsParams, G extends Geometry>
     createEmptyGeometry(geometry?: G): G
     createRenderObject(ctx: VisualContext, group: Unit.SymmetryGroup, geometry: Geometry, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<UnitsRenderObject>
     updateValues(values: RenderableValues, newProps: PD.Values<P>): void
+    updateBoundingSphere(values: RenderableValues, geometry: Geometry): void
 }
 
 export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryBuilder<P, Geometry>): UnitsVisual<P> {
     const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder
-    const { createEmptyGeometry, createRenderObject, updateValues } = builder
+    const { createEmptyGeometry, createRenderObject, updateValues, updateBoundingSphere } = builder
     const updateState = VisualUpdateState.create()
 
     let renderObject: UnitsRenderObject | undefined
@@ -88,53 +82,78 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
         renderObject = await createRenderObject(ctx, group, geometry, locationIt, theme, currentProps)
     }
 
-    async function update(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}) {
+    async function update(ctx: VisualContext, group: Unit.SymmetryGroup, theme: Theme, props: Partial<PD.Values<P>> = {}) {
         if (!renderObject) return
 
         const newProps = Object.assign({}, currentProps, props, { structure: currentStructure })
-        const unit = currentGroup.units[0]
+        const unit = group.units[0]
 
         locationIt.reset()
         VisualUpdateState.reset(updateState)
         setUpdateState(updateState, newProps, currentProps, theme, currentTheme)
 
+        if (!ColorTheme.areEqual(theme.color, currentTheme.color)) {
+            // console.log('new colorTheme')
+            updateState.updateColor = true
+        }
+        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) {
+            // console.log('new unitKinds')
+            updateState.createGeometry = true
+        }
+
+        if (group.transformHash !== currentGroup.transformHash) {
+            // console.log('new transformHash')
+            if (group.units.length !== currentGroup.units.length || updateState.updateColor) {
+                updateState.updateTransform = true
+            } else {
+                updateState.updateMatrix = true
+            }
+        }
+
+        // check if the conformation of unit.model has changed
         const newConformationId = Unit.conformationId(unit)
         if (newConformationId !== currentConformationId) {
+            // console.log('new conformation')
             currentConformationId = newConformationId
             updateState.createGeometry = true
         }
 
-        if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true
-
-        if (ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true
-        if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true
-
         //
 
         if (updateState.updateTransform) {
-            locationIt = createLocationIterator(currentGroup)
+            // console.log('update transform')
+            locationIt = createLocationIterator(group)
             const { instanceCount, groupCount } = locationIt
-            createUnitsTransform(currentGroup, renderObject.values)
             createMarkers(instanceCount * groupCount, renderObject.values)
             updateState.updateColor = true
+            updateState.updateMatrix = true
+        }
+
+        if (updateState.updateMatrix) {
+            // console.log('update matrix')
+            createUnitsTransform(group, renderObject.values)
         }
 
         if (updateState.createGeometry) {
+            // console.log('update geometry')
             geometry = includesUnitKind(newProps.unitKinds, unit)
                 ? await createGeometry(ctx, unit, currentStructure, theme, newProps, geometry)
                 : createEmptyGeometry(geometry)
             ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
+            updateBoundingSphere(renderObject.values, geometry)
             updateState.updateColor = true
         }
 
         if (updateState.updateSize) {
             // not all geometries have size data, so check here
             if ('uSize' in renderObject.values) {
+                // console.log('update size')
                 await createSizes(ctx.runtime, locationIt, theme.size, renderObject.values)
             }
         }
 
         if (updateState.updateColor) {
+            // console.log('update color')
             await createColors(ctx.runtime, locationIt, theme.color, renderObject.values)
         }
 
@@ -143,6 +162,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
 
         currentProps = newProps
         currentTheme = theme
+        currentGroup = group
     }
 
     return {
@@ -161,11 +181,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
                 await create(ctx, group, theme, props)
             } else {
                 // console.log('unit-visual update')
-                if (group && !sameGroupConformation(group, currentGroup)) {
-                    // console.log('unit-visual new conformation')
-                    currentGroup = group
-                }
-                await update(ctx, theme, props)
+                await update(ctx, group || currentGroup, theme, props)
             }
         },
         getLoci(pickingId: PickingId) {
@@ -183,7 +199,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB
             }
 
             let changed = false
-            if (isEveryLoci(loci)) {
+            if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructure)) {
                 changed = apply(Interval.ofBounds(0, groupCount * instanceCount))
             } else {
                 changed = mark(loci, { structure: currentStructure, group: currentGroup }, apply)
@@ -224,7 +240,8 @@ export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVis
         },
         createEmptyGeometry: Mesh.createEmpty,
         createRenderObject: createUnitsMeshRenderObject,
-        updateValues: Mesh.updateValues
+        updateValues: Mesh.updateValues,
+        updateBoundingSphere: Mesh.updateBoundingSphere
     })
 }
 
@@ -246,7 +263,8 @@ export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPoi
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
-        updateValues: Points.updateValues
+        updateValues: Points.updateValues,
+        updateBoundingSphere: Points.updateBoundingSphere
     })
 }
 
@@ -268,7 +286,8 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true
         },
-        updateValues: Lines.updateValues
+        updateValues: Lines.updateValues,
+        updateBoundingSphere: Lines.updateBoundingSphere
     })
 }
 
@@ -290,6 +309,7 @@ export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(build
             builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme)
             if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true
         },
-        updateValues: DirectVolume.updateValues
+        updateValues: DirectVolume.updateValues,
+        updateBoundingSphere: DirectVolume.updateBoundingSphere
     })
 }

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

@@ -203,12 +203,12 @@ function markCarbohydrate(loci: Loci, structure: Structure, apply: (interval: In
         for (const e of loci.elements) {
             OrderedSet.forEach(e.indices, v => {
                 const { model, elements } = e.unit
-                const { index, offsets } = model.atomicHierarchy.residueAtomSegments        
+                const { index, offsets } = model.atomicHierarchy.residueAtomSegments
                 const rI = index[elements[v]]
                 const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI])
                 const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1)
                 const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax)
-                if(!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
+                if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return
                 const eI = getAnomericCarbon(e.unit, rI)
                 if (eI !== undefined) {
                     const idx = getElementIndex(e.unit, eI)

+ 3 - 3
src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts

@@ -34,9 +34,9 @@ async function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, st
             const l = terminalLinks[edgeIndex]
             if (l.fromCarbohydrate) {
                 Vec3.copy(posA, elements[l.carbohydrateIndex].geometry.center)
-                l.elementUnit.conformation.position(l.elementIndex, posB)
+                l.elementUnit.conformation.position(l.elementUnit.elements[l.elementIndex], posB)
             } else {
-                l.elementUnit.conformation.position(l.elementIndex, posA)
+                l.elementUnit.conformation.position(l.elementUnit.elements[l.elementIndex], posA)
                 Vec3.copy(posB, elements[l.carbohydrateIndex].geometry.center)
             }
         },
@@ -123,7 +123,7 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num
                 l.elementUnit, l.elementIndex,
                 carb.unit, carbIndex as StructureElement.UnitIndex
             )
-        ])    
+        ])
     }
     return EmptyLoci
 }

+ 0 - 2
src/mol-repr/structure/visual/element-sphere.ts

@@ -10,11 +10,9 @@ import { VisualUpdateState } from '../../util';
 import { createElementSphereMesh, markElement, getElementLoci, StructureElementIterator } from './util/element';
 import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
-import { BuiltInSizeThemeOptions, getBuiltInSizeThemeParams } from 'mol-theme/size';
 
 export const ElementSphereParams = {
     ...UnitsMeshParams,
-    sizeTheme: PD.Mapped('physical', BuiltInSizeThemeOptions, getBuiltInSizeThemeParams),
     sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }),
 }

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

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Unit, Structure } from 'mol-model/structure';
+import { Unit, Structure, ElementIndex } from 'mol-model/structure';
 import { UnitsVisual } from '../representation';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra';
 import { Segmentation } from 'mol-data/int';
@@ -71,7 +71,7 @@ async function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structu
             if (isNucleic(moleculeType)) {
                 const parentId = modifiedResidues.parentId.get(compId)
                 if (parentId !== undefined) compId = parentId
-                let idx1 = -1, idx2 = -1, idx3 = -1, idx4 = -1, idx5 = -1, idx6 = -1
+                let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1
                 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor
 
                 if (isPurinBase(compId)) {

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

@@ -62,7 +62,7 @@ async function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, s
 
         interpolateCurveSegment(state, v, tension, shift)
 
-        if ((isSheet && !v.secStrucChange) || !isSheet) {
+        if ((isSheet && !v.secStrucLast) || !isSheet) {
             const size = theme.size.size(v.center) * sizeFactor
             const depth = depthFactor * size
             const width = widthFactor * size

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

@@ -52,7 +52,7 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure:
         const isNucleicType = isNucleic(v.moleculeType)
         const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
         const isHelix = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix)
-        const tension = (isNucleicType || isSheet) ? 0.5 : 0.9
+        const tension = isNucleicType ? 0.5 : 0.9
         const shift = isNucleicType ? 0.3 : 0.5
 
         interpolateCurveSegment(state, v, tension, shift)
@@ -62,8 +62,8 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure:
 
         if (isSheet) {
             const height = width * aspectRatio
-            const arrowHeight = v.secStrucChange ? height * arrowFactor : 0
-            addSheet(builder, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true)
+            const arrowHeight = v.secStrucLast ? height * arrowFactor : 0
+            addSheet(builder, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, v.secStrucFirst, v.secStrucLast)
         } else {
             let height: number
             if (isHelix) {
@@ -74,7 +74,7 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure:
             } else {
                 height = width
             }
-            addTube(builder, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true)
+            addTube(builder, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, v.secStrucFirst, v.secStrucLast)
         }
 
         if (i % 10000 === 0 && ctx.runtime.shouldUpdate) {

+ 30 - 16
src/mol-repr/structure/visual/util/polymer/trace-iterator.ts

@@ -13,6 +13,7 @@ import SortedRanges from 'mol-data/int/sorted-ranges';
 import { CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse';
 import { getAtomicMoleculeType, getElementIndexForAtomRole } from 'mol-model/structure/util';
 import { getPolymerRanges } from '../polymer';
+import { AtomicConformation } from 'mol-model/structure/model/properties/atomic';
 
 /**
  * Iterates over individual residues/coarse elements in polymers of a unit while
@@ -30,20 +31,22 @@ export function PolymerTraceIterator(unit: Unit): Iterator<PolymerTraceElement>
 interface PolymerTraceElement {
     center: StructureElement
     first: boolean, last: boolean
+    secStrucFirst: boolean, secStrucLast: boolean
     secStrucType: SecondaryStructureType
-    secStrucChange: boolean
     moleculeType: MoleculeType
 
     p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3
     d12: Vec3, d23: Vec3
 }
 
+const SecStrucTypeNA = SecondaryStructureType.create(SecondaryStructureType.Flag.NA)
+
 function createPolymerTraceElement (unit: Unit): PolymerTraceElement {
     return {
         center: StructureElement.create(unit),
         first: false, last: false,
-        secStrucType: SecondaryStructureType.create(SecondaryStructureType.Flag.NA),
-        secStrucChange: false,
+        secStrucFirst: false, secStrucLast: false,
+        secStrucType: SecStrucTypeNA,
         moleculeType: MoleculeType.unknown,
         p0: Vec3.zero(), p1: Vec3.zero(), p2: Vec3.zero(), p3: Vec3.zero(), p4: Vec3.zero(),
         d12: Vec3.create(1, 0, 0), d23: Vec3.create(1, 0, 0),
@@ -57,10 +60,15 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
     private residueIt: Segmentation.SegmentIterator<ResidueIndex>
     private polymerSegment: Segmentation.Segment<ResidueIndex>
+    private secondaryStructureType: ArrayLike<SecondaryStructureType>
     private residueSegmentMin: ResidueIndex
     private residueSegmentMax: ResidueIndex
+    private prevSecStrucType: SecondaryStructureType
+    private currSecStrucType: SecondaryStructureType
+    private nextSecStrucType: SecondaryStructureType
     private state: AtomicPolymerTraceIteratorState = AtomicPolymerTraceIteratorState.nextPolymer
     private residueAtomSegments: Segmentation<ElementIndex, ResidueIndex>
+    private atomicConformation: AtomicConformation
 
     private p0 = Vec3.zero();
     private p1 = Vec3.zero();
@@ -78,13 +86,13 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     hasNext: boolean = false;
 
     private pos(target: Vec3, index: number) {
-        target[0] = this.unit.model.atomicConformation.x[index]
-        target[1] = this.unit.model.atomicConformation.y[index]
-        target[2] = this.unit.model.atomicConformation.z[index]
+        target[0] = this.atomicConformation.x[index]
+        target[1] = this.atomicConformation.y[index]
+        target[2] = this.atomicConformation.z[index]
     }
 
     private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<ResidueIndex>) {
-        const { index } = this.unit.model.atomicHierarchy.residueAtomSegments
+        const { index } = this.residueAtomSegments
         this.residueSegmentMin = index[this.unit.elements[polymerSegment.start]]
         this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]]
     }
@@ -111,8 +119,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     }
 
     private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) {
-        const ss = this.unit.model.properties.secondaryStructure.type[residueIndex]
-        if (SecondaryStructureType.is(ss, SecondaryStructureType.Flag.Beta)) {
+        if (SecondaryStructureType.is(this.currSecStrucType, SecondaryStructureType.Flag.Beta)) {
             Vec3.scale(out, Vec3.add(out, p1, Vec3.add(out, p3, Vec3.add(out, p2, p2))), 1/4)
         } else {
             Vec3.copy(out, p2)
@@ -129,6 +136,8 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
                 this.updateResidueSegmentRange(this.polymerSegment)
                 if (residueIt.hasNext) {
                     this.state = AtomicPolymerTraceIteratorState.nextResidue
+                    this.currSecStrucType = SecStrucTypeNA
+                    this.nextSecStrucType = this.secondaryStructureType[this.residueSegmentMin]
                     break
                 }
             }
@@ -136,7 +145,17 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
 
         if (this.state === AtomicPolymerTraceIteratorState.nextResidue) {
             const { index: residueIndex } = residueIt.move();
+            this.prevSecStrucType = this.currSecStrucType
+            this.currSecStrucType = this.nextSecStrucType
+            this.nextSecStrucType = residueIt.hasNext ? this.secondaryStructureType[residueIndex + 1] : SecStrucTypeNA
+
+            value.secStrucType = this.currSecStrucType
             value.center.element = this.getElementIndex(residueIndex, 'trace')
+            value.first = residueIndex === this.residueSegmentMin
+            value.last = residueIndex === this.residueSegmentMax
+            value.secStrucFirst = this.prevSecStrucType !== this.currSecStrucType
+            value.secStrucLast = this.currSecStrucType !== this.nextSecStrucType
+            value.moleculeType = getAtomicMoleculeType(this.unit.model, residueIndex)
 
             this.pos(this.p0, this.getElementIndex(residueIndex - 3 as ResidueIndex, 'trace'))
             this.pos(this.p1, this.getElementIndex(residueIndex - 2 as ResidueIndex, 'trace'))
@@ -151,8 +170,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
             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]
-
             this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndex - 2 as ResidueIndex)
             this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndex - 1 as ResidueIndex)
             this.setControlPoint(value.p2, this.p2, this.p3, this.p4, residueIndex)
@@ -162,11 +179,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
             Vec3.copy(value.d12, this.v12)
             Vec3.copy(value.d23, this.v23)
 
-            value.first = residueIndex === this.residueSegmentMin
-            value.last = residueIndex === this.residueSegmentMax
-            value.secStrucChange = this.unit.model.properties.secondaryStructure.key[residueIndex] !== this.unit.model.properties.secondaryStructure.key[residueIndex + 1]
-            value.moleculeType = getAtomicMoleculeType(this.unit.model, residueIndex)
-
             if (!residueIt.hasNext) {
                 this.state = AtomicPolymerTraceIteratorState.nextPolymer
             }
@@ -178,7 +190,9 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement>
     }
 
     constructor(private unit: Unit.Atomic) {
+        this.atomicConformation = unit.model.atomicConformation
         this.residueAtomSegments = unit.model.atomicHierarchy.residueAtomSegments
+        this.secondaryStructureType = unit.model.properties.secondaryStructure.type
         this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements)
         this.residueIt = Segmentation.transientSegments(this.residueAtomSegments, unit.elements);
         this.value = createPolymerTraceElement(unit)

+ 3 - 0
src/mol-repr/util.ts

@@ -10,6 +10,7 @@ import { VisualQuality } from 'mol-geo/geometry/geometry';
 
 export interface VisualUpdateState {
     updateTransform: boolean
+    updateMatrix: boolean
     updateColor: boolean
     updateSize: boolean
     createGeometry: boolean
@@ -18,6 +19,7 @@ export namespace VisualUpdateState {
     export function create(): VisualUpdateState {
         return {
             updateTransform: false,
+            updateMatrix: false,
             updateColor: false,
             updateSize: false,
             createGeometry: false
@@ -25,6 +27,7 @@ export namespace VisualUpdateState {
     }
     export function reset(state: VisualUpdateState) {
         state.updateTransform = false
+        state.updateMatrix = false
         state.updateColor = false
         state.updateSize = false
         state.createGeometry = false

+ 14 - 5
src/mol-repr/volume/direct-volume.ts

@@ -6,7 +6,7 @@
 
 import { VolumeData } from 'mol-model/volume'
 import { RuntimeContext } from 'mol-task'
-import { VolumeVisual, VolumeRepresentation } from './representation';
+import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
 import { createDirectVolumeRenderObject } from 'mol-gl/render-object';
 import { EmptyLoci } from 'mol-model/loci';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -19,7 +19,7 @@ import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume';
 import { Geometry, createRenderableState } from 'mol-geo/geometry/geometry';
 import { VisualUpdateState } from 'mol-repr/util';
-import { VisualContext } from 'mol-repr/representation';
+import { VisualContext, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
 
 function getBoundingBox(gridDimension: Vec3, transform: Mat4) {
@@ -178,10 +178,19 @@ export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> {
             const state = createRenderableState(props)
             return createDirectVolumeRenderObject(values, state)
         },
-        updateValues: DirectVolume.updateValues
+        updateValues: DirectVolume.updateValues,
+        updateBoundingSphere: DirectVolume.updateBoundingSphere
     })
 }
 
-export function DirectVolumeRepresentation(): VolumeRepresentation<DirectVolumeParams> {
-    return VolumeRepresentation('Direct Volume', getDirectVolumeParams, DirectVolumeVisual)
+export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> {
+    return VolumeRepresentation('Direct Volume', ctx, getParams, DirectVolumeVisual)
+}
+
+export const DirectVolumeRepresentationProvider: VolumeRepresentationProvider<DirectVolumeParams> = {
+    label: 'Direct Volume',
+    description: 'Direct volume rendering of volumetric data.',
+    factory: DirectVolumeRepresentation,
+    getParams: getDirectVolumeParams,
+    defaultValues: PD.getDefaultValues(DirectVolumeParams)
 }

+ 14 - 5
src/mol-repr/volume/isosurface-mesh.ts

@@ -6,7 +6,7 @@
  */
 
 import { VolumeData } from 'mol-model/volume'
-import { VolumeVisual, VolumeRepresentation } from './representation';
+import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation';
 import { createMeshRenderObject } from 'mol-gl/render-object';
 import { EmptyLoci } from 'mol-model/loci';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
@@ -16,7 +16,7 @@ import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { createIdentityTransform } from 'mol-geo/geometry/transform-data';
 import { createRenderableState } from 'mol-geo/geometry/geometry';
 import { VisualUpdateState } from 'mol-repr/util';
-import { VisualContext } from 'mol-repr/representation';
+import { VisualContext, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation';
 import { Theme, ThemeRegistryContext } from 'mol-theme/theme';
 
 interface VolumeIsosurfaceProps {
@@ -63,10 +63,19 @@ export function IsosurfaceVisual(): VolumeVisual<IsosurfaceParams> {
             const state = createRenderableState(props)
             return createMeshRenderObject(values, state)
         },
-        updateValues: Mesh.updateValues
+        updateValues: Mesh.updateValues,
+        updateBoundingSphere: Mesh.updateBoundingSphere
     })
 }
 
-export function IsosurfaceRepresentation(): VolumeRepresentation<IsosurfaceParams> {
-    return VolumeRepresentation('Isosurface', getIsosurfaceParams, IsosurfaceVisual)
+export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceParams>): VolumeRepresentation<IsosurfaceParams> {
+    return VolumeRepresentation('Isosurface', ctx, getParams, IsosurfaceVisual)
+}
+
+export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceParams> = {
+    label: 'Isosurface',
+    description: 'Displays an isosurface of volumetric data.',
+    factory: IsosurfaceRepresentation,
+    getParams: getIsosurfaceParams,
+    defaultValues: PD.getDefaultValues(IsosurfaceParams)
 }

+ 4 - 1
src/mol-repr/volume/registry.ts

@@ -6,6 +6,8 @@
 
 import { RepresentationProvider, RepresentationRegistry } from '../representation';
 import { VolumeData } from 'mol-model/volume';
+import { IsosurfaceRepresentationProvider } from './isosurface-mesh';
+import { DirectVolumeRepresentationProvider } from './direct-volume';
 
 export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData> {
     constructor() {
@@ -18,7 +20,8 @@ export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeD
 }
 
 export const BuiltInVolumeRepresentations = {
-    // TODO
+    'isosurface': IsosurfaceRepresentationProvider,
+    'direct-volume': DirectVolumeRepresentationProvider,
 }
 export type BuiltInVolumeRepresentationsName = keyof typeof BuiltInVolumeRepresentations
 export const BuiltInVolumeRepresentationsNames = Object.keys(BuiltInVolumeRepresentations)

+ 25 - 18
src/mol-repr/volume/representation.ts

@@ -19,7 +19,7 @@ import { LocationIterator } from 'mol-geo/util/location-iterator';
 import { NullLocation } from 'mol-model/location';
 import { VisualUpdateState } from 'mol-repr/util';
 import { ValueCell } from 'mol-util';
-import { Theme, createTheme } from 'mol-theme/theme';
+import { Theme, createEmptyTheme } from 'mol-theme/theme';
 import { Subject } from 'rxjs';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { }
@@ -36,12 +36,13 @@ interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
 
 interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry> extends VolumeVisualBuilder<P, G> {
     createRenderObject(ctx: VisualContext, geometry: G, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<VolumeRenderObject>
-    updateValues(values: RenderableValues, newProps: PD.Values<P>): void
+    updateValues(values: RenderableValues, newProps: PD.Values<P>): void,
+    updateBoundingSphere(values: RenderableValues, geometry: G): void
 }
 
 export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>): VolumeVisual<P> {
     const { defaultProps, createGeometry, getLoci, mark, setUpdateState } = builder
-    const { createRenderObject, updateValues } = builder
+    const { createRenderObject, updateValues, updateBoundingSphere } = builder
     const updateState = VisualUpdateState.create()
 
     let currentProps: PD.Values<P>
@@ -67,6 +68,7 @@ export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeomet
         if (updateState.createGeometry) {
             geometry = await createGeometry(ctx, currentVolume, currentProps, geometry)
             ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry))
+            updateBoundingSphere(renderObject.values, geometry)
         }
 
         updateValues(renderObject.values, newProps)
@@ -143,25 +145,25 @@ export const VolumeParams = {
 }
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> {
     let version = 0
     const updated = new Subject<number>()
+    const _state = Representation.createState()
     let visual: VolumeVisual<P>
 
     let _volume: VolumeData
     let _props: PD.Values<P>
     let _params: P
-    let _theme: Theme
+    let _theme = createEmptyTheme()
     let busy = false
 
-    function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
+    function createOrUpdate(props: Partial<PD.Values<P>> = {}, volume?: VolumeData) {
         if (volume && volume !== _volume) {
             _params = getParams(ctx, volume)
             _volume = volume
             if (!_props) _props = PD.getDefaultValues(_params)
         }
         _props = Object.assign({}, _props, props)
-        _theme = createTheme(ctx, _props, {}, _theme)
 
         return Task.create('VolumeRepresentation.create', async runtime => {
             // TODO queue it somehow
@@ -172,11 +174,11 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
             } else if (volume && !visual) {
                 busy = true
                 visual = visualCtor(volume)
-                await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, volume)
+                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
                 busy = false
             } else {
                 busy = true
-                await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, volume)
+                await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume)
                 busy = false
             }
             updated.next(version++)
@@ -191,16 +193,19 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
         return visual ? visual.mark(loci, action) : false
     }
 
-    function destroy() {
-        if (visual) visual.destroy()
+    function setState(state: Partial<Representation.State>) {
+        if (state.visible !== undefined && visual) visual.setVisibility(state.visible)
+        if (state.pickable !== undefined && visual) visual.setPickable(state.pickable)
+
+        Representation.updateState(_state, state)
     }
 
-    function setVisibility(value: boolean) {
-        if (visual) visual.setVisibility(value)
+    function setTheme(theme: Theme) {
+        _theme = theme
     }
 
-    function setPickable(value: boolean) {
-        if (visual) visual.setPickable(value)
+    function destroy() {
+        if (visual) visual.destroy()
     }
 
     return {
@@ -213,12 +218,14 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP
         },
         get props () { return _props },
         get params() { return _params },
-        get updated() { return updated },
+        get state() { return _state },
+        get theme() { return _theme },
+        updated,
         createOrUpdate,
+        setState,
+        setTheme,
         getLoci,
         mark,
-        setVisibility,
-        setPickable,
         destroy
     }
 }

+ 51 - 8
src/mol-state/action.ts

@@ -31,27 +31,29 @@ namespace StateAction {
     }
 
     export interface ApplyParams<A extends StateObject = StateObject, P extends {} = {}> {
+        ref: string,
         cell: StateObjectCell,
         a: A,
         state: State,
         params: P
     }
 
-    export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> {
-        readonly from: StateObject.Ctor[],
-        readonly display?: { readonly name: string, readonly description?: string },
-
+    export interface DefinitionBase<A extends StateObject = StateObject, T = any, P extends {} = {}> {
         /**
          * Apply an action that modifies the State specified in Params.
          */
-        apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
-
-        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
+        run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean
     }
 
+    export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
+        readonly from: StateObject.Ctor[],
+        readonly display: { readonly name: string, readonly description?: string },
+        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }
+    }
+
     export function create<A extends StateObject, T, P extends {} = {}>(definition: Definition<A, T, P>): StateAction<A, T, P> {
         const action: StateAction<A, T, P> = {
             create(params) { return { action, params }; },
@@ -67,10 +69,51 @@ namespace StateAction {
             from: def.from,
             display: def.display,
             params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'],
-            apply({ cell, state, params }) {
+            run({ cell, state, params }) {
                 const tree = state.build().to(cell.transform.ref).apply(transformer, params);
                 return state.update(tree);
             }
         })
     }
+
+    export namespace Builder {
+        export interface Type<A extends StateObject.Ctor, P extends { }> {
+            from?: A | A[],
+            params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
+            display?: string | { name: string, description?: string }
+        }
+
+        export interface Root {
+            <A extends StateObject.Ctor, P extends { }>(info: Type<A, P>): Define<StateObject.From<A>, PD.Normalize<P>>
+        }
+
+        export interface Define<A extends StateObject, P> {
+            <T>(def: DefinitionBase<A, T, P> | DefinitionBase<A, T, P>['run']): StateAction<A, T, P>,
+        }
+
+        function root(info: Type<any, any>): Define<any, any> {
+            return def => create({
+                from: info.from instanceof Array
+                    ? info.from
+                    : !!info.from ? [info.from] : [],
+                display: typeof info.display === 'string'
+                    ? { name: info.display }
+                    : !!info.display
+                    ? info.display
+                    : { name: 'Unnamed State Action' },
+                params: typeof info.params === 'object'
+                    ? () => info.params as any
+                    : !!info.params
+                    ? info.params as any
+                    : void 0,
+                ...(typeof def === 'function'
+                    ? { run: def }
+                    : def)
+            });
+        }
+
+        export const build: Root = (info: any) => root(info);
+    }
+
+    export const build = Builder.build;
 }

+ 4 - 3
src/mol-state/object.ts

@@ -23,12 +23,13 @@ namespace StateObject {
     }
 
     export type Type<Cls extends string = string> = { name: string, typeClass: Cls }
-    export type Ctor = { new(...args: any[]): StateObject, type: any }
+    export type Ctor<T extends StateObject = StateObject> = { new(...args: any[]): T, type: any }
+    export type From<C extends Ctor> = C extends Ctor<infer T> ? T : never
 
     export function create<Data, T extends Type>(type: T) {
-        return class implements StateObject<Data, T> {
+        return class O implements StateObject<Data, T> {
             static type = type;
-            static is(obj?: StateObject): obj is StateObject<Data, T> { return !!obj && type === obj.type; }
+            static is(obj?: StateObject): obj is O { return !!obj && type === obj.type; }
             id = UUID.create22();
             type = type;
             label: string;

+ 43 - 25
src/mol-state/state.ts

@@ -104,7 +104,7 @@ class State {
             if (!cell) throw new Error(`'${ref}' does not exist.`);
             if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`);
 
-            return runTask(action.definition.apply({ cell, a: cell.obj!, params, state: this }, this.globalContext), ctx);
+            return runTask(action.definition.run({ ref, cell, a: cell.obj!, params, state: this }, this.globalContext), ctx);
         });
     }
 
@@ -224,7 +224,7 @@ async function update(ctx: UpdateContext) {
         }
 
         if (hasCurrent) {
-            const newCurrent = findNewCurrent(ctx, current, deletes);
+            const newCurrent = findNewCurrent(ctx.oldTree, current, deletes, ctx.cells);
             ctx.parent.setCurrent(newCurrent);
         }
 
@@ -270,14 +270,14 @@ async function update(ctx: UpdateContext) {
         await updateSubtree(ctx, root);
     }
 
-    let newCurrent: Transform.Ref | undefined;
+    let newCurrent: Transform.Ref | undefined = ctx.newCurrent;
     // Raise object updated events
     for (const update of ctx.results) {
         if (update.action === 'created') {
             ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
-            if (!ctx.hadError) {
+            if (!ctx.newCurrent) {
                 const transform = ctx.tree.transforms.get(update.ref);
-                if (!transform.props || !transform.props.isGhost) newCurrent = update.ref;
+                if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref;
             }
         } else if (update.action === 'updated') {
             ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
@@ -286,8 +286,19 @@ async function update(ctx: UpdateContext) {
         }
     }
 
-    if (ctx.newCurrent) ctx.parent.setCurrent(ctx.newCurrent);
-    else if (newCurrent) ctx.parent.setCurrent(newCurrent);
+    if (newCurrent) ctx.parent.setCurrent(newCurrent);
+    else {
+        // check if old current or its parent hasn't become null
+        const current = ctx.parent.current;
+        const currentCell = ctx.cells.get(current);
+        if (currentCell && (
+                currentCell.obj === StateObject.Null
+            || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) {
+            newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells);
+            ctx.parent.setCurrent(newCurrent);
+        }
+    }
+
 
     return deletes.length > 0 || roots.length > 0 || ctx.changed;
 }
@@ -304,6 +315,8 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells:
         s.roots.push(n.ref);
         return false;
     }
+    // nothing below a Null object can be an update root
+    if (cell && cell.obj === StateObject.Null) return false;
     return true;
 }
 
@@ -369,12 +382,12 @@ function initCells(ctx: UpdateContext, roots: Ref[]) {
     return initCtx.added;
 }
 
-function findNewCurrent(ctx: UpdateContext, start: Ref, deletes: Ref[]) {
+function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<Ref, StateObjectCell>) {
     const deleteSet = new Set(deletes);
-    return _findNewCurrent(ctx.oldTree, start, deleteSet);
+    return _findNewCurrent(tree, start, deleteSet, cells);
 }
 
-function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref {
+function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref {
     if (ref === Transform.RootRef) return ref;
 
     const node = tree.transforms.get(ref)!;
@@ -387,6 +400,10 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref {
         if (s.done) break;
 
         if (deletes.has(s.value)) continue;
+        const cell = cells.get(s.value);
+        if (!cell || cell.status === 'error' || cell.obj === StateObject.Null) {
+            continue;
+        }
 
         const t = tree.transforms.get(s.value);
         if (t.props && t.props.isGhost) continue;
@@ -402,17 +419,19 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref {
     }
 
     if (prevCandidate) return prevCandidate;
-    return _findNewCurrent(tree, node.parent, deletes);
+    return _findNewCurrent(tree, node.parent, deletes, cells);
 }
 
 /** Set status and error text of the cell. Remove all existing objects in the subtree. */
-function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) {
-    ctx.hadError = true;
-    (ctx.parent as any as { errorFree: boolean }).errorFree = false;
+function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined, silent: boolean) {
+    if (!silent) {
+        ctx.hadError = true;
+        (ctx.parent as any as { errorFree: boolean }).errorFree = false;
+    }
 
     if (errorText) {
         setCellStatus(ctx, ref, 'error', errorText);
-        ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText });
+        if (!silent) ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText });
     }
 
     const cell = ctx.cells.get(ref)!;
@@ -428,7 +447,7 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) {
     while (true) {
         const next = children.next();
         if (next.done) return;
-        doError(ctx, next.value, void 0);
+        doError(ctx, next.value, void 0, silent);
     }
 }
 
@@ -438,6 +457,8 @@ type UpdateNodeResult =
     | { ref: Ref, action: 'replaced', oldObj?: StateObject, obj: StateObject }
     | { action: 'none' }
 
+const ParentNullErrorText = 'Parent is null';
+
 async function updateSubtree(ctx: UpdateContext, root: Ref) {
     setCellStatus(ctx, root, 'processing');
 
@@ -453,30 +474,27 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) {
         ctx.results.push(update);
         if (update.action === 'created') {
             isNull = update.obj === StateObject.Null;
-            ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
         } else if (update.action === 'updated') {
             isNull = update.obj === StateObject.Null;
-            ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
         } else if (update.action === 'replaced') {
             isNull = update.obj === StateObject.Null;
-            ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
+            if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
         }
     } catch (e) {
         ctx.changed = true;
         if (!ctx.hadError) ctx.newCurrent = root;
-        doError(ctx, root, '' + e);
+        doError(ctx, root, '' + e, false);
         return;
     }
 
-    // Do not continue the updates if the object is null
-    // TODO: set the states to something "nicer"?
-    if (isNull) return;
-
     const children = ctx.tree.children.get(root).values();
     while (true) {
         const next = children.next();
         if (next.done) return;
-        await updateSubtree(ctx, next.value);
+        if (isNull) doError(ctx, next.value, ParentNullErrorText, true);
+        else await updateSubtree(ctx, next.value);
     }
 }
 

+ 69 - 7
src/mol-state/transformer.ts

@@ -9,6 +9,7 @@ import { StateObject } from './object';
 import { Transform } from './transform';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { StateAction } from './action';
+import { capitalize } from 'mol-util/string';
 
 export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
     apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
@@ -44,17 +45,19 @@ export namespace Transformer {
         cache: unknown
     }
 
+    export interface AutoUpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
+        a: A,
+        b: B,
+        oldParams: P,
+        newParams: P
+    }
+
     export enum UpdateResult { Unchanged, Updated, Recreate }
 
     /** Specify default control descriptors for the parameters */
     // export type ParamsDefinition<A extends StateObject = StateObject, P = any> = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any }
 
-    export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
-        readonly name: string,
-        readonly from: StateObject.Ctor[],
-        readonly to: StateObject.Ctor[],
-        readonly display?: { readonly name: string, readonly description?: string },
-
+    export interface DefinitionBase<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
         /**
          * Apply the actual transformation. It must be pure (i.e. with no side effects).
          * Returns a task that produces the result of the result directly.
@@ -68,7 +71,8 @@ export namespace Transformer {
          */
         update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult,
 
-        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
+        /** Determine if the transformer can be applied automatically on UI change. Default is false. */
+        canAutoUpdate?(params: AutoUpdateParams<A, B, P>, globalCtx: unknown): boolean,
 
         /** Test if the transform can be applied to a given node */
         isApplicable?(a: A, globalCtx: unknown): boolean,
@@ -80,6 +84,14 @@ export namespace Transformer {
         readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P }
     }
 
+    export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> extends DefinitionBase<A, B, P> {
+        readonly name: string,
+        readonly from: StateObject.Ctor[],
+        readonly to: StateObject.Ctor[],
+        readonly display: { readonly name: string, readonly description?: string },
+        params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any },
+    }
+
     const registry = new Map<Id, Transformer<any, any>>();
     const fromTypeIndex: Map<StateObject.Type, Transformer[]> = new Map();
 
@@ -130,10 +142,60 @@ export namespace Transformer {
         return <A extends StateObject, B extends StateObject, P extends {} = {}>(definition: Definition<A, B, P>) => create(namespace, definition);
     }
 
+    export function builderFactory(namespace: string) {
+        return Builder.build(namespace);
+    }
+
+    export namespace Builder {
+        export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }> {
+            name: string,
+            from: A | A[],
+            to: B | B[],
+            params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
+            display?: string | { name: string, description?: string }
+        }
+
+        export interface Root {
+            <A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }>(info: Type<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, PD.Normalize<P>>
+        }
+
+        export interface Define<A extends StateObject, B extends StateObject, P> {
+            (def: DefinitionBase<A, B, P>): Transformer<A, B, P>
+        }
+
+        function root(namespace: string, info: Type<any, any, any>): Define<any, any, any> {
+            return def => create(namespace, {
+                name: info.name,
+                from: info.from instanceof Array ? info.from : [info.from],
+                to: info.to instanceof Array ? info.to : [info.to],
+                display: typeof info.display === 'string'
+                    ? { name: info.display }
+                    : !!info.display
+                    ? info.display
+                    : { name: capitalize(info.name.replace(/[-]/g, ' ')) },
+                params: typeof info.params === 'object'
+                    ? () => info.params as any
+                    : !!info.params
+                    ? info.params as any
+                    : void 0,
+                ...def
+            });
+        }
+
+        export function build(namespace: string): Root {
+            return (info: any) => root(namespace, info);
+        }
+    }
+
+    export function build(namespace: string): Builder.Root {
+        return Builder.build(namespace);
+    }
+
     export const ROOT = create<any, any, {}>('build-in', {
         name: 'root',
         from: [],
         to: [],
+        display: { name: 'Root' },
         apply() { throw new Error('should never be applied'); },
         update() { return UpdateResult.Unchanged; }
     })

+ 2 - 2
src/mol-state/tree/builder.ts

@@ -59,9 +59,9 @@ namespace StateTreeBuilder {
             return new To(this.state, t.ref, this.root);
         }
 
-        update<T extends Transformer<A, any, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root
+        update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root
         update(params: any): Root
-        update<T extends Transformer<A, any, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) {
+        update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) {
             let params: any;
             if (provider) {
                 const old = this.state.tree.transforms.get(this.ref)!;

+ 14 - 12
src/mol-theme/color.ts

@@ -8,7 +8,7 @@ import { Color } from 'mol-util/color';
 import { Location } from 'mol-model/location';
 import { ColorType } from 'mol-geo/geometry/color-data';
 import { CarbohydrateSymbolColorThemeProvider } from './color/carbohydrate-symbol';
-import { UniformColorTheme, UniformColorThemeProvider } from './color/uniform';
+import { UniformColorThemeProvider } from './color/uniform';
 import { deepEqual } from 'mol-util';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { ThemeDataContext } from './theme';
@@ -17,6 +17,7 @@ import { CrossLinkColorThemeProvider } from './color/cross-link';
 import { ElementIndexColorThemeProvider } from './color/element-index';
 import { ElementSymbolColorThemeProvider } from './color/element-symbol';
 import { MoleculeTypeColorThemeProvider } from './color/molecule-type';
+import { PolymerIdColorThemeProvider } from './color/polymer-id';
 import { PolymerIndexColorThemeProvider } from './color/polymer-index';
 import { ResidueNameColorThemeProvider } from './color/residue-name';
 import { SecondaryStructureColorThemeProvider } from './color/secondary-structure';
@@ -31,27 +32,31 @@ export type LocationColor = (location: Location, isSecondary: boolean) => Color
 export type ColorThemeProps = { [k: string]: any }
 
 export { ColorTheme }
-interface ColorTheme<P extends ColorThemeProps = {}> {
+interface ColorTheme<P extends PD.Params = {}> {
+    readonly factory: ColorTheme.Factory<P>
     readonly granularity: ColorType
     readonly color: LocationColor
-    readonly props: Readonly<P>
+    readonly props: Readonly<PD.Values<P>>
     readonly description?: string
     readonly legend?: Readonly<ScaleLegend | TableLegend>
 }
 namespace ColorTheme {
     export type Props = { [k: string]: any }
-    export const Empty = UniformColorTheme({}, { value: Color(0xCCCCCC) })
+    export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
+    export const EmptyFactory = () => Empty
+    const EmptyColor = Color(0xCCCCCC)
+    export const Empty: ColorTheme<{}> = { factory: EmptyFactory, granularity: 'uniform', color: () => EmptyColor, props: {} }
 
     export function areEqual(themeA: ColorTheme, themeB: ColorTheme) {
-        return themeA === themeB && deepEqual(themeA.props, themeB.props)
+        return themeA.factory === themeB.factory && deepEqual(themeA.props, themeB.props)
     }
 
     export interface Provider<P extends PD.Params> {
         readonly label: string
-        readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<PD.Values<P>>
+        readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P>
         readonly getParams: (ctx: ThemeDataContext) => P
     }
-    export const EmptyProvider: Provider<{}> = { label: '', factory: () => Empty, getParams: () => ({}) }
+    export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) }
 
     export class Registry {
         private _list: { name: string, provider: Provider<any> }[] = []
@@ -96,6 +101,7 @@ export const BuiltInColorThemes = {
     'element-index': ElementIndexColorThemeProvider,
     'element-symbol': ElementSymbolColorThemeProvider,
     'molecule-type': MoleculeTypeColorThemeProvider,
+    'polymer-id': PolymerIdColorThemeProvider,
     'polymer-index': PolymerIndexColorThemeProvider,
     'residue-name': ResidueNameColorThemeProvider,
     'secondary-structure': SecondaryStructureColorThemeProvider,
@@ -103,8 +109,4 @@ export const BuiltInColorThemes = {
     'shape-group': ShapeGroupColorThemeProvider,
     'unit-index': UnitIndexColorThemeProvider,
     'uniform': UniformColorThemeProvider,
-}
-export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes
-export const BuiltInColorThemeNames = Object.keys(BuiltInColorThemes)
-export const BuiltInColorThemeOptions = BuiltInColorThemeNames.map(n => [n, n] as [BuiltInColorThemeName, string])
-export const getBuiltInColorThemeParams = (name: string, ctx: ThemeDataContext = {}) => PD.Group((BuiltInColorThemes as { [k: string]: ColorTheme.Provider<any> })[name].getParams(ctx))
+}

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