ソースを参照

wip, repr updating

Alexander Rose 7 年 前
コミット
64c211ceaa

+ 31 - 52
package-lock.json

@@ -90,7 +90,7 @@
       "resolved": "https://registry.npmjs.org/@types/jss/-/jss-9.5.2.tgz",
       "integrity": "sha512-EX87yNYcisXO5BU9tT7stB7OGuDJyV3JwtMwhfUprrmHwYKWh9a3vchAy6DYzUSbmTA7bD46h8qata5jP1V7Zw==",
       "requires": {
-        "csstype": "2.1.1",
+        "csstype": "2.3.0",
         "indefinite-observable": "1.0.1"
       }
     },
@@ -1796,11 +1796,6 @@
         "lazy-cache": "1.0.4"
       }
     },
-    "chain-function": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.0.tgz",
-      "integrity": "sha1-DUqzfn4Y6tC9xHuSB2QRjOWHM9w="
-    },
     "chalk": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz",
@@ -2501,9 +2496,9 @@
       }
     },
     "csstype": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.1.1.tgz",
-      "integrity": "sha512-YsNVkaQtmsauSmlwqr/3EhJamZIObOcqfOgOmPuQxEXhsSvt/1/4M+bqN9xpsSEJqT2TWfTs2mPWrmwp0iQX6g=="
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.3.0.tgz",
+      "integrity": "sha512-+iowf+HbYUKV65+HjAhXkx4KH6IFpIxnBlO0maKsXmBIHJXEndaTRYPVL4pEwtK6+1zRvkXo+WD1tRFKygMHQg=="
     },
     "cyclist": {
       "version": "0.2.2",
@@ -5254,6 +5249,11 @@
       "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
       "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
     },
+    "hoist-non-react-statics": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
+      "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
+    },
     "home-or-tmp": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@@ -7115,9 +7115,9 @@
       }
     },
     "material-ui": {
-      "version": "1.0.0-beta.41",
-      "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-1.0.0-beta.41.tgz",
-      "integrity": "sha512-eUFD7mrrAIHPY/KGX23SDWJE5Xg5TAm4rH+E71yrKjmVOwrXT1YBxw1LUw9r+Y4XQD7rhGmGrUQan5CJAWUG5A==",
+      "version": "1.0.0-beta.43",
+      "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-1.0.0-beta.43.tgz",
+      "integrity": "sha512-6prcG6pwOBAH3s44GJg1WiylByz/TSL6oXlDXnBW1Sb0mSjL1ezfdZj4nSO2qjrPyfDm9sjwxaR9ZoVIHxU17g==",
       "requires": {
         "@types/jss": "9.5.2",
         "@types/react-transition-group": "2.0.8",
@@ -7140,32 +7140,13 @@
         "prop-types": "15.6.1",
         "react-event-listener": "0.5.3",
         "react-jss": "8.4.0",
-        "react-lifecycles-compat": "1.1.4",
-        "react-popper": "0.8.3",
+        "react-lifecycles-compat": "2.0.2",
+        "react-popper": "0.10.1",
         "react-scrollbar-size": "2.1.0",
-        "react-transition-group": "2.3.0",
+        "react-transition-group": "2.3.1",
         "recompose": "0.26.0",
         "scroll": "2.0.3",
         "warning": "3.0.0"
-      },
-      "dependencies": {
-        "hoist-non-react-statics": {
-          "version": "2.5.0",
-          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
-          "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
-        },
-        "react-transition-group": {
-          "version": "2.3.0",
-          "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.0.tgz",
-          "integrity": "sha512-OU3/swEL8y233u5ajTn3FIcQQ/b3XWjLXB6e2LnM1OK5JATtsyfJvPTZ8c/dawHNqjUltcdHRSpgMtPe7v07pw==",
-          "requires": {
-            "chain-function": "1.0.0",
-            "dom-helpers": "3.3.1",
-            "loose-envify": "1.3.1",
-            "prop-types": "15.6.1",
-            "warning": "3.0.0"
-          }
-        }
       }
     },
     "md5.js": {
@@ -8806,24 +8787,17 @@
         "jss-preset-default": "4.3.0",
         "prop-types": "15.6.1",
         "theming": "1.3.0"
-      },
-      "dependencies": {
-        "hoist-non-react-statics": {
-          "version": "2.5.0",
-          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
-          "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
-        }
       }
     },
     "react-lifecycles-compat": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-1.1.4.tgz",
-      "integrity": "sha512-g3pdexIqkn+CVvSpYIoyON8zUbF9kgfhp672gyz7wQ7PQyXVmJtah+GDYqpHpOrdwex3F77iv+alq79iux9HZw=="
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-2.0.2.tgz",
+      "integrity": "sha512-BPksUj7VMAAFhcCw79sZA0Ow/LTAEjs3Sio1AQcuwLeOP+ua0f/08Su2wyiW+JjDDH6fRqNy3h5CLXh21u1mVg=="
     },
     "react-popper": {
-      "version": "0.8.3",
-      "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.8.3.tgz",
-      "integrity": "sha1-D3MzMTfJ+wr27EB00tBYWgoEYeE=",
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.10.1.tgz",
+      "integrity": "sha1-ah8llfr/2ncQW+1OiezyJgekxFI=",
       "requires": {
         "popper.js": "1.14.3",
         "prop-types": "15.6.1"
@@ -8840,6 +8814,16 @@
         "stifle": "1.0.4"
       }
     },
+    "react-transition-group": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.1.tgz",
+      "integrity": "sha512-hu4/LAOFSKjWt1+1hgnOv3ldxmt6lvZGTWz4KUkFrqzXrNDIVSu6txIcPszw7PNduR8en9YTN55JLRyd/L1ZiQ==",
+      "requires": {
+        "dom-helpers": "3.3.1",
+        "loose-envify": "1.3.1",
+        "prop-types": "15.6.1"
+      }
+    },
     "read-chunk": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz",
@@ -8975,11 +8959,6 @@
         "symbol-observable": "1.2.0"
       },
       "dependencies": {
-        "hoist-non-react-statics": {
-          "version": "2.5.0",
-          "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz",
-          "integrity": "sha512-6Bl6XsDT1ntE0lHbIhr4Kp2PGcleGZ66qu5Jqk8lc0Xc/IeG6gVLmwUGs/K0Us+L8VWoKgj0uWdPMataOsm31w=="
-        },
         "symbol-observable": {
           "version": "1.2.0",
           "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",

+ 1 - 1
package.json

@@ -86,7 +86,7 @@
     "argparse": "^1.0.10",
     "express": "^4.16.3",
     "gl": "^4.0.4",
-    "material-ui": "^1.0.0-beta.41",
+    "material-ui": "^1.0.0-beta.43",
     "node-fetch": "^2.1.2",
     "react": "^16.3.2",
     "react-dom": "^16.3.2",

+ 60 - 0
src/apps/render-test/components/color-theme.tsx

@@ -0,0 +1,60 @@
+/**
+ * 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 { WithStyles } from 'material-ui/styles';
+import { MenuItem } from 'material-ui/Menu';
+import { InputLabel } from 'material-ui/Input';
+import { FormControl } from 'material-ui/Form';
+import Select from 'material-ui/Select';
+
+import State, { ColorTheme as _ColorTheme } from '../state'
+import Observer from './observer';
+
+interface ColorThemeState {
+    loading: boolean
+    name: _ColorTheme
+}
+
+export default class ColorTheme extends Observer<{ state: State } & WithStyles, ColorThemeState> {
+    state = { loading: false, name: 'element-symbol' as _ColorTheme }
+
+    componentDidMount() {
+        this.subscribe(this.props.state.loading, value => {
+           this.setState({ loading: value });
+        });
+        this.subscribe(this.props.state.colorTheme, value => {
+            this.setState({ name: value });
+         });
+    }
+
+    handleNameChange = (event: React.ChangeEvent<any>) => {
+        this.props.state.colorTheme.next(event.target.value)
+    }
+
+    render() {
+        const { classes } = this.props;
+
+        const items = Object.keys(_ColorTheme).map((name, idx) => {
+            return <MenuItem key={idx} value={name}>{name}</MenuItem>
+        })
+
+        return <FormControl className={classes.formControl}>
+            <InputLabel htmlFor='color-theme-name'>Color Theme</InputLabel>
+            <Select
+                className={classes.selectField}
+                value={this.state.name}
+                onChange={this.handleNameChange}
+                inputProps={{
+                    name: 'name',
+                    id: 'color-theme-name',
+                }}
+            >
+                {items}
+            </Select>
+        </FormControl>
+    }
+}

+ 60 - 0
src/apps/render-test/components/detail.tsx

@@ -0,0 +1,60 @@
+/**
+ * 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 { WithStyles } from 'material-ui/styles';
+import { MenuItem } from 'material-ui/Menu';
+import { InputLabel } from 'material-ui/Input';
+import { FormControl } from 'material-ui/Form';
+import Select from 'material-ui/Select';
+
+import State from '../state'
+import Observer from './observer';
+
+interface DetailState {
+    loading: boolean
+    value: number
+}
+
+export default class Detail extends Observer<{ state: State } & WithStyles, DetailState> {
+    state = { loading: false, value: 2 }
+
+    componentDidMount() {
+        this.subscribe(this.props.state.loading, value => {
+           this.setState({ loading: value });
+        });
+        this.subscribe(this.props.state.detail, value => {
+            this.setState({ value });
+         });
+    }
+
+    handleValueChange = (event: React.ChangeEvent<any>) => {
+        this.props.state.detail.next(event.target.value)
+    }
+
+    render() {
+        const { classes } = this.props;
+
+        const items = [0, 1, 2].map((value, idx) => {
+            return <MenuItem key={idx} value={value}>{value.toString()}</MenuItem>
+        })
+
+        return <FormControl className={classes.formControl}>
+            <InputLabel htmlFor='detail-value'>Detail</InputLabel>
+            <Select
+                className={classes.selectField}
+                value={this.state.value}
+                onChange={this.handleValueChange}
+                inputProps={{
+                    name: 'value',
+                    id: 'detail-value',
+                }}
+            >
+                {items}
+            </Select>
+        </FormControl>
+    }
+}

+ 70 - 0
src/apps/render-test/components/visibility.tsx

@@ -0,0 +1,70 @@
+/**
+ * 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 { WithStyles } from 'material-ui/styles';
+import { FormLabel, FormControl, FormGroup, FormControlLabel } from 'material-ui/Form';
+import Checkbox from 'material-ui/Checkbox';
+
+import State from '../state'
+import Observer from './observer';
+
+interface VisibilityState {
+    loading: boolean
+    spacefill: boolean
+    point: boolean
+}
+
+export default class Visibility extends Observer<{ state: State } & WithStyles, VisibilityState> {
+    state = { loading: false, spacefill: true, point: true }
+
+    componentDidMount() {
+        this.subscribe(this.props.state.loading, value => {
+           this.setState({ loading: value });
+        });
+        this.subscribe(this.props.state.spacefillVisibility, value => {
+            this.setState({ spacefill: value });
+        });
+        this.subscribe(this.props.state.pointVisibility, value => {
+            this.setState({ point: value });
+        });
+    }
+
+    handleChange = (event: React.ChangeEvent<any>) => {
+        switch (event.target.name) {
+            case 'point': this.props.state.pointVisibility.next(event.target.checked); break;
+            case 'spacefill': this.props.state.spacefillVisibility.next(event.target.checked); break;
+        }
+    }
+
+    render() {
+        const { classes } = this.props
+
+        return <div className={classes.formControl}>
+            <FormControl component='fieldset'>
+                <FormLabel component='legend'>Visibility</FormLabel>
+                <FormGroup>
+                    <FormControlLabel
+                        control={<Checkbox
+                            checked={this.state.point}
+                            onChange={this.handleChange}
+                            name='point'
+                        />}
+                        label='Point'
+                    />
+                    <FormControlLabel
+                        control={<Checkbox
+                            checked={this.state.spacefill}
+                            onChange={this.handleChange}
+                            name='spacefill'
+                        />}
+                        label='Spacefill'
+                    />
+                </FormGroup>
+            </FormControl>
+        </div>
+    }
+}

+ 80 - 23
src/apps/render-test/state.ts

@@ -24,12 +24,52 @@ import { getStructuresFromPdbId, log } from './utils'
 import { StructureRepresentation } from 'mol-geo/representation/structure';
 // import Cylinder from 'mol-geo/primitive/cylinder';
 
+
+export const ColorTheme = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {}
+}
+export type ColorTheme = keyof typeof ColorTheme
+
 export default class State {
     viewer: Viewer
     pdbId = '4cup'
     initialized = new BehaviorSubject<boolean>(false)
     loading = new BehaviorSubject<boolean>(false)
 
+    colorTheme = new BehaviorSubject<ColorTheme>('chain-id')
+    detail = new BehaviorSubject<number>(2)
+
+    pointVisibility = new BehaviorSubject<boolean>(true)
+    spacefillVisibility = new BehaviorSubject<boolean>(true)
+
+    pointRepr: StructureRepresentation<PointProps>
+    spacefillRepr: StructureRepresentation<SpacefillProps>
+
+    constructor() {
+        this.colorTheme.subscribe(() => this.update())
+        this.detail.subscribe(() => this.update())
+
+        this.pointVisibility.subscribe(() => this.updateVisibility())
+        this.spacefillVisibility.subscribe(() => this.updateVisibility())
+    }
+
+    getSpacefillProps (): SpacefillProps {
+        return {
+            detail: this.detail.getValue(),
+            colorTheme: { name: this.colorTheme.getValue() },
+        }
+    }
+
+    getPointProps (): PointProps {
+        return {
+            colorTheme: { name: this.colorTheme.getValue() },
+            sizeTheme: { name: 'uniform', value: 0.1 }
+        }
+    }
+
     async initRenderer (canvas: HTMLCanvasElement, container: HTMLDivElement) {
         this.viewer = Viewer.create(canvas, container)
         this.initialized.next(true)
@@ -38,40 +78,57 @@ export default class State {
     }
 
     async loadPdbId () {
-        const { viewer, pdbId } = this
+        const { viewer, pdbId, loading } = this
         viewer.clear()
 
         if (pdbId.length !== 4) return
-        this.loading.next(true)
+        loading.next(true)
 
         const structures = await getStructuresFromPdbId(pdbId)
         const struct = await Run(Symmetry.buildAssembly(structures[0], '1'), log, 100)
 
-        const structPointRepr = StructureRepresentation(Point)
-        const pointProps: PointProps = {
-            // colorTheme: { name: 'uniform', value: 0xFF4411 },
-            colorTheme: { name: 'chain-id' },
-            sizeTheme: { name: 'uniform', value: 0.1 }
-        }
-        await Run(structPointRepr.create(struct, pointProps), log, 100)
-        structPointRepr.renderObjects.forEach(viewer.add)
-
-        const structSpacefillRepr = StructureRepresentation(Spacefill)
-        const spacefillProps: SpacefillProps = {
-            detail: 1,
-            // colorTheme: { name: 'uniform', value: 0xFF4411 },
-            // colorTheme: { name: 'instance-index' },
-            // colorTheme: { name: 'element-symbol' },
-            // colorTheme: { name: 'atom-index' },
-            colorTheme: { name: 'chain-id' },
-        }
-        await Run(structSpacefillRepr.create(struct, spacefillProps), log, 100)
-        structSpacefillRepr.renderObjects.forEach(viewer.add)
+        this.pointRepr = StructureRepresentation(Point)
+        await Run(this.pointRepr.create(struct, this.getPointProps()), log, 100)
+        viewer.add(this.pointRepr)
+
+        this.spacefillRepr = StructureRepresentation(Spacefill)
+        await Run(this.spacefillRepr.create(struct, this.getSpacefillProps()), log, 100)
+        viewer.add(this.spacefillRepr)
 
+        this.updateVisibility()
         viewer.requestDraw()
         console.log(viewer.stats)
 
-        this.loading.next(false)
+        loading.next(false)
+    }
+
+    async update () {
+        if (!this.spacefillRepr) return
+        await Run(this.spacefillRepr.update(this.getSpacefillProps()), log, 100)
+        await Run(this.pointRepr.update(this.getPointProps()), log, 100)
+        this.viewer.add(this.spacefillRepr)
+        this.viewer.add(this.pointRepr)
+        this.viewer.requestDraw()
+        console.log(this.viewer.stats)
+    }
+
+    updateVisibility () {
+        if (!this.viewer) return
+        if (this.pointRepr) {
+            if (this.pointVisibility.getValue()) {
+                this.viewer.show(this.pointRepr)
+            } else {
+                this.viewer.hide(this.pointRepr)
+            }
+        }
+        if (this.spacefillRepr) {
+            if (this.spacefillVisibility.getValue()) {
+                this.viewer.show(this.spacefillRepr)
+            } else {
+                this.viewer.hide(this.spacefillRepr)
+            }
+        }
+        this.viewer.requestDraw()
     }
 }
 

+ 19 - 3
src/apps/render-test/ui.tsx

@@ -11,11 +11,16 @@ import Toolbar from 'material-ui/Toolbar';
 import AppBar from 'material-ui/AppBar';
 import Drawer from 'material-ui/Drawer';
 
+import State from './state'
+
 import Viewport from './components/viewport'
 import FileInput from './components/file-input'
-import State from './state'
+import ColorTheme from './components/color-theme'
+import Detail from './components/detail'
+import Visibility from './components/visibility'
 
-const styles: StyleRulesCallback<any> = (theme: Theme) => ({
+
+const styles: StyleRulesCallback = (theme: Theme) => ({
     root: {
         flexGrow: 1,
         height: 830,
@@ -38,12 +43,16 @@ const styles: StyleRulesCallback<any> = (theme: Theme) => ({
         minWidth: 0, // So the Typography noWrap works
     },
     toolbar: theme.mixins.toolbar,
+    formControl: {
+        margin: theme.spacing.unit,
+        width: 200,
+    },
     textField: {
         marginLeft: theme.spacing.unit,
         marginRight: theme.spacing.unit,
         width: 200,
     },
-});
+} as any);
 
 const decorate = withStyles(styles);
 
@@ -66,6 +75,13 @@ class UI extends React.Component<{ state: State } & WithStyles, {  }> {
                 <Drawer variant='permanent' classes={{ paper: classes.drawerPaper, }}>
                     <div className={classes.toolbar} />
                     <FileInput state={state} classes={classes}></FileInput>
+                    <form className={classes.root} autoComplete='off'>
+                        <div>
+                            <ColorTheme state={state} classes={classes}></ColorTheme>
+                            <Detail state={state} classes={classes}></Detail>
+                            <Visibility state={state} classes={classes}></Visibility>
+                        </div>
+                    </form>
                 </Drawer>
                 <main className={classes.content}>
                     <div className={classes.toolbar} />

+ 1 - 1
src/apps/structure-info/index.ts

@@ -22,7 +22,7 @@ async function parseCif(data: string|Uint8Array) {
 }
 
 async function getPdb(pdb: string) {
-    //const data = await fetch(`https://files.rcsb.org/download/${pdb}.cif`)
+    // const data = await fetch(`https://files.rcsb.org/download/${pdb}.cif`)
     const data = await fetch(`http://www.ebi.ac.uk/pdbe/static/entry/${pdb}_updated.cif`);
     const parsed = await parseCif(await data.text())
     return CIF.schema.mmCIF(parsed.result.blocks[0])

+ 34 - 17
src/mol-geo/representation/structure/index.ts

@@ -15,26 +15,32 @@ export interface RepresentationProps {
 
 }
 
-export interface UnitsRepresentation<Props> {
-    renderObjects: ReadonlyArray<RenderObject>,
-    create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: Props) => Task<void>,
-    update: (props: RepresentationProps) => boolean,
+export interface UnitsRepresentation<Props = {}> {
+    renderObjects: ReadonlyArray<RenderObject>
+    create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: Props) => Task<void>
+    update: (props: RepresentationProps) => Task<boolean>
 }
 
-export interface StructureRepresentation<Props> {
-    renderObjects: ReadonlyArray<RenderObject>,
-    create: (structure: Structure, props?: Props) => Task<void>,
-    update: (elements: ElementSet, props: Props) => boolean
+export interface StructureRepresentation<Props = {}> {
+    renderObjects: ReadonlyArray<RenderObject>
+    create: (structure: Structure, props?: Props) => Task<void>
+    update: (props: Props) => Task<void>
+}
+
+interface GroupRepresentation<T> {
+    repr: UnitsRepresentation<T>
+    units: Unit[]
+    elementGroup: ElementGroup
 }
 
 export function StructureRepresentation<Props>(reprCtor: () => UnitsRepresentation<Props>): StructureRepresentation<Props> {
     const renderObjects: RenderObject[] = []
-    const unitReprs: UnitsRepresentation<Props>[] = []
+    const groupReprs: GroupRepresentation<Props>[] = []
 
     return {
         renderObjects,
         create(structure: Structure, props: Props = {} as Props) {
-            return Task.create('StructureRepresentation', async ctx => {
+            return Task.create('StructureRepresentation.create', async ctx => {
                 const { elements, units } = structure;
                 const uniqueGroups = EquivalenceClasses<number, { unit: Unit, group: ElementGroup }>(
                     ({ unit, group }) => ElementGroup.hashCode(group),
@@ -57,25 +63,36 @@ export function StructureRepresentation<Props>(reprCtor: () => UnitsRepresentati
 
                 // console.log({ uniqueGroups, uniqueTransformations })
 
-                for (let i = 0, _i = uniqueGroups.groups.length; i < _i; i++) {
+                for (let i = 0, il = uniqueGroups.groups.length; i < il; i++) {
                     const groupUnits: Unit[] = []
                     const group = uniqueGroups.groups[i]
                     // console.log('group', i)
-                    for (let j = 0, _j = group.length; j < _j; j++) {
+                    for (let j = 0, jl = group.length; j < jl; j++) {
                         groupUnits.push(units[group[j]])
                     }
                     const elementGroup = ElementSet.groupFromUnitIndex(elements, group[0])
                     const repr = reprCtor()
-                    unitReprs.push(repr)
-                    await ctx.update({ message: 'Building units...', current: i, max: _i });
+                    groupReprs.push({ repr, units: groupUnits, elementGroup })
+                    await ctx.update({ message: 'Building units...', current: i, max: il });
                     await ctx.runChild(repr.create(groupUnits, elementGroup, props));
                     renderObjects.push(...repr.renderObjects)
                 }
             });
         },
-        update(elements: ElementSet, props: RepresentationProps) {
-            // TODO check model.id, conformation.id, unit.id, elementGroup(.hashCode/.areEqual)
-            return false
+        update(props: Props) {
+            return Task.create('StructureRepresentation.update', async ctx => {
+                // TODO check model.id, conformation.id, unit.id, elementGroup(.hashCode/.areEqual)
+                renderObjects.length = 0 // clear
+                for (let i = 0, il = groupReprs.length; i < il; ++i) {
+                    const groupRepr = groupReprs[i]
+                    const { repr, units, elementGroup } = groupRepr
+                    await ctx.update({ message: 'Updating units...', current: i, max: il });
+                    if (!await ctx.runChild(repr.update(props))) {
+                        await ctx.runChild(repr.create(units, elementGroup, props))
+                    }
+                    renderObjects.push(...repr.renderObjects)
+                }
+            })
         }
     }
 }

+ 44 - 33
src/mol-geo/representation/structure/point.ts

@@ -6,7 +6,7 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { createPointRenderObject, RenderObject } from 'mol-gl/scene'
+import { createPointRenderObject, RenderObject, PointRenderObject } from 'mol-gl/scene'
 
 import { OrderedSet } from 'mol-data/int'
 import { Unit, ElementGroup } from 'mol-model/structure';
@@ -40,50 +40,61 @@ export function createPointVertices(unit: Unit, elementGroup: ElementGroup) {
 
 export default function Point(): UnitsRepresentation<PointProps> {
     const renderObjects: RenderObject[] = []
+    let points: PointRenderObject
 
     return {
         renderObjects,
-        create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: PointProps = {}) => Task.create('Spacefill', async ctx => {
-            const { colorTheme, sizeTheme } = { ...DefaultPointProps, ...props }
-            const elementCount = OrderedSet.size(elementGroup.elements)
-            const unitCount = units.length
+        create(units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: PointProps = {}) {
+            return Task.create('Point.create', async ctx => {
+                renderObjects.length = 0 // clear
 
-            const vertexMap = VertexMap.create(
-                elementCount,
-                elementCount + 1,
-                fillSerial(new Uint32Array(elementCount)),
-                fillSerial(new Uint32Array(elementCount + 1))
-            )
+                const { colorTheme, sizeTheme } = { ...DefaultPointProps, ...props }
+                const elementCount = OrderedSet.size(elementGroup.elements)
+                const unitCount = units.length
 
-            await ctx.update('Computing point vertices');
-            const vertices = createPointVertices(units[0], elementGroup)
+                const vertexMap = VertexMap.create(
+                    elementCount,
+                    elementCount + 1,
+                    fillSerial(new Uint32Array(elementCount)),
+                    fillSerial(new Uint32Array(elementCount + 1))
+                )
 
-            await ctx.update('Computing point transforms');
-            const transforms = createTransforms(units)
+                await ctx.update('Computing point vertices');
+                const vertices = createPointVertices(units[0], elementGroup)
 
-            await ctx.update('Computing point colors');
-            const color = createColors(units, elementGroup, vertexMap, colorTheme)
+                await ctx.update('Computing point transforms');
+                const transforms = createTransforms(units)
 
-            await ctx.update('Computing point sizes');
-            const size = createSizes(units, elementGroup, vertexMap, sizeTheme)
+                await ctx.update('Computing point colors');
+                const color = createColors(units, elementGroup, vertexMap, colorTheme)
 
-            const points = createPointRenderObject({
-                objectId: 0,
+                await ctx.update('Computing point sizes');
+                const size = createSizes(units, elementGroup, vertexMap, sizeTheme)
 
-                position: ValueCell.create(vertices),
-                id: ValueCell.create(fillSerial(new Float32Array(unitCount))),
-                size,
-                color,
-                transform: ValueCell.create(transforms),
+                points = createPointRenderObject({
+                    objectId: 0,
 
-                instanceCount: unitCount,
-                elementCount,
-                positionCount: vertices.length / 3,
+                    position: ValueCell.create(vertices),
+                    id: ValueCell.create(fillSerial(new Float32Array(unitCount))),
+                    size,
+                    color,
+                    transform: ValueCell.create(transforms),
 
-                usePointSizeAttenuation: true
+                    instanceCount: unitCount,
+                    elementCount,
+                    positionCount: vertices.length / 3,
+
+                    usePointSizeAttenuation: true
+                })
+                renderObjects.push(points)
+            })
+        },
+        update(props: RepresentationProps) {
+            return Task.create('Point.update', async ctx => {
+                if (!points) return false
+
+                return false
             })
-            renderObjects.push(points)
-        }),
-        update: (props: RepresentationProps) => false
+        }
     }
 }

+ 36 - 28
src/mol-geo/representation/structure/spacefill.ts

@@ -6,7 +6,7 @@
 
 import { ValueCell } from 'mol-util/value-cell'
 
-import { RenderObject, createMeshRenderObject } from 'mol-gl/scene'
+import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/scene'
 // import { createColorTexture } from 'mol-gl/util';
 import { Vec3, Mat4 } from 'mol-math/linear-algebra'
 import { OrderedSet } from 'mol-data/int'
@@ -59,44 +59,52 @@ function createSpacefillMesh(unit: Unit, elementGroup: ElementGroup, detail: num
 
 export default function Spacefill(): UnitsRepresentation<SpacefillProps> {
     const renderObjects: RenderObject[] = []
+    let spheres: MeshRenderObject
 
     return {
         renderObjects,
-        create: (units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: SpacefillProps = {}) => Task.create('Spacefill', async ctx => {
-            const { detail, colorTheme } = { ...DefaultSpacefillProps, ...props }
+        create(units: ReadonlyArray<Unit>, elementGroup: ElementGroup, props: SpacefillProps = {}) {
+            return Task.create('Spacefill.create', async ctx => {
+                renderObjects.length = 0 // clear
 
-            const unitCount = units.length
-            const elementCount = OrderedSet.size(elementGroup.elements)
+                const { detail, colorTheme } = { ...DefaultSpacefillProps, ...props }
 
-            await ctx.update('Computing spacefill mesh');
-            const mesh = await ctx.runChild(createSpacefillMesh(units[0], elementGroup, detail))
-            // console.log(mesh)
+                await ctx.update('Computing spacefill mesh');
+                const mesh = await ctx.runChild(createSpacefillMesh(units[0], elementGroup, detail))
+                // console.log(mesh)
 
-            const vertexMap = VertexMap.fromMesh(mesh)
+                const vertexMap = VertexMap.fromMesh(mesh)
 
-            await ctx.update('Computing spacefill transforms');
-            const transforms = createTransforms(units)
+                await ctx.update('Computing spacefill transforms');
+                const transforms = createTransforms(units)
 
-            await ctx.update('Computing spacefill colors');
-            const color = createColors(units, elementGroup, vertexMap, colorTheme)
+                await ctx.update('Computing spacefill colors');
+                const color = createColors(units, elementGroup, vertexMap, colorTheme)
 
-            const spheres = createMeshRenderObject({
-                objectId: 0,
+                spheres = createMeshRenderObject({
+                    objectId: 0,
 
-                position: mesh.vertexBuffer,
-                normal: mesh.normalBuffer as ValueCell<Float32Array>,
-                color: color,
-                id: mesh.idBuffer as ValueCell<Float32Array>,
-                transform: ValueCell.create(transforms),
-                index: mesh.indexBuffer,
+                    position: mesh.vertexBuffer,
+                    normal: mesh.normalBuffer as ValueCell<Float32Array>,
+                    color: color,
+                    id: mesh.idBuffer as ValueCell<Float32Array>,
+                    transform: ValueCell.create(transforms),
+                    index: mesh.indexBuffer,
 
-                instanceCount: unitCount,
-                indexCount: mesh.triangleCount,
-                elementCount: elementCount,
-                positionCount: mesh.vertexCount
+                    instanceCount: units.length,
+                    indexCount: mesh.triangleCount,
+                    elementCount: OrderedSet.size(elementGroup.elements),
+                    positionCount: mesh.vertexCount
+                })
+                renderObjects.push(spheres)
             })
-            renderObjects.push(spheres)
-        }),
-        update: (props: RepresentationProps) => false
+        },
+        update(props: RepresentationProps) {
+            return Task.create('Spacefill.update', async ctx => {
+                if (!spheres) return false
+
+                return false
+            })
+        }
     }
 }

+ 2 - 3
src/mol-gl/renderer.ts

@@ -78,13 +78,12 @@ namespace Renderer {
             baseContext(state => {
                 regl.clear({ color: [0, 0, 0, 1] })
                 // TODO painters sort, filter visible, filter picking, visibility culling?
-                scene.forEach(r => {
-                    r.draw()
+                scene.forEach((r, o) => {
+                    if (o.visible) r.draw()
                 })
             })
         }
 
-        // TODO animate, draw, requestDraw
         return {
             add: (o: RenderObject) => {
                 scene.add(o)

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

@@ -16,15 +16,16 @@ function getNextId() {
 
 export type RenderData = { [k: string]: ValueCell<Helpers.TypedArray> }
 
-export interface MeshRenderObject { id: number, type: 'mesh', props: MeshRenderable.Data }
-export interface PointRenderObject { id: number, type: 'point', props: PointRenderable.Data }
+export interface BaseRenderObject { id: number, type: string, props: {}, visible: boolean }
+export interface MeshRenderObject extends BaseRenderObject { type: 'mesh', props: MeshRenderable.Data }
+export interface PointRenderObject extends BaseRenderObject { type: 'point', props: PointRenderable.Data }
 export type RenderObject = MeshRenderObject | PointRenderObject
 
 export function createMeshRenderObject(props: MeshRenderable.Data): MeshRenderObject {
-    return { id: getNextId(), type: 'mesh', props }
+    return { id: getNextId(), type: 'mesh', props, visible: true }
 }
 export function createPointRenderObject(props: PointRenderable.Data): PointRenderObject {
-    return { id: getNextId(), type: 'point', props }
+    return { id: getNextId(), type: 'point', props, visible: true }
 }
 
 export function createRenderable(regl: REGL.Regl, o: RenderObject) {
@@ -38,39 +39,38 @@ interface Scene {
     add: (o: RenderObject) => void
     remove: (o: RenderObject) => void
     clear: () => void
-    forEach: (callbackFn: (value: Renderable) => void) => void
+    forEach: (callbackFn: (value: Renderable, key: RenderObject) => void) => void
     count: number
 }
 
 namespace Scene {
     export function create(regl: REGL.Regl): Scene {
-        const renderableList: Renderable[] = []
-        const objectIdRenderableMap: { [k: number]: Renderable } = {}
+        const renderableMap = new Map<RenderObject, Renderable>()
 
         return {
             add: (o: RenderObject) => {
-                const renderable = createRenderable(regl, o)
-                renderableList.push(renderable)
-                objectIdRenderableMap[o.id] = renderable
+                if (!renderableMap.has(o)) {
+                    renderableMap.set(o, createRenderable(regl, o))
+                } else {
+                    console.warn(`RenderObject with id '${o.id}' already present`)
+                }
             },
             remove: (o: RenderObject) => {
-                if (o.id in objectIdRenderableMap) {
-                    objectIdRenderableMap[o.id].dispose()
-                    delete objectIdRenderableMap[o.id]
+                const renderable = renderableMap.get(o)
+                if (renderable) {
+                    renderable.dispose()
+                    renderableMap.delete(o)
                 }
             },
             clear: () => {
-                for (const id in objectIdRenderableMap) {
-                    objectIdRenderableMap[id].dispose()
-                    delete objectIdRenderableMap[id]
-                }
-                renderableList.length = 0
+                renderableMap.forEach(renderable => renderable.dispose())
+                renderableMap.clear()
             },
-            forEach: (callbackFn: (value: Renderable) => void) => {
-                renderableList.forEach(callbackFn)
+            forEach: (callbackFn: (value: Renderable, key: RenderObject) => void) => {
+                renderableMap.forEach(callbackFn)
             },
             get count() {
-                return renderableList.length
+                return renderableMap.size
             }
         }
     }

+ 38 - 0
src/mol-util/set.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// TODO remove Array.from workaround when targeting ES6
+
+/** Test if set a contains all elements of set b. */
+export function isSuperset<T>(setA: Set<T>, setB: Set<T>) {
+    for (const elm of Array.from(setB)) {
+        if (!setA.has(elm)) return false;
+    }
+    return true;
+}
+
+/** Create set containing elements of both set a and set b. */
+export function union<T>(setA: Set<T>, setB: Set<T>) {
+    const union = new Set(setA);
+    for (const elem of Array.from(setB)) union.add(elem);
+    return union;
+}
+
+/** Create set containing elements of set a that are also in set b. */
+export function intersection<T>(setA: Set<T>, setB: Set<T>) {
+    const intersection = new Set();
+    for (const elem of Array.from(setB)) {
+        if (setA.has(elem)) intersection.add(elem);
+    }
+    return intersection;
+}
+
+/** Create set containing elements of set a that are not in set b. */
+export function difference<T>(setA: Set<T>, setB: Set<T>) {
+    const difference = new Set(setA);
+    for (const elem of Array.from(setB)) difference.delete(elem);
+    return difference;
+}

+ 34 - 7
src/mol-view/viewer.ts

@@ -6,8 +6,10 @@
 
 import { Vec3 } from 'mol-math/linear-algebra'
 import InputObserver from 'mol-util/input/input-observer'
+import * as SetUtils from 'mol-util/set'
 import Renderer, { RendererStats } from 'mol-gl/renderer'
 import { RenderObject } from 'mol-gl/scene'
+import { StructureRepresentation } from 'mol-geo/representation/structure';
 
 import TrackballControls from './controls/trackball'
 import { Viewport } from './camera/util'
@@ -15,11 +17,14 @@ import { PerspectiveCamera } from './camera/perspective'
 import { resizeCanvas } from './util';
 
 interface Viewer {
-    add: (o: RenderObject) => void
-    remove: (o: RenderObject) => void
+    hide: (repr: StructureRepresentation) => void
+    show: (repr: StructureRepresentation) => void
+
+    add: (repr: StructureRepresentation) => void
+    remove: (repr: StructureRepresentation) => void
     clear: () => void
-    draw: () => void
 
+    draw: () => void
     requestDraw: () => void
     animate: () => void
 
@@ -42,6 +47,8 @@ function getWebGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLCon
 
 namespace Viewer {
     export function create(canvas: HTMLCanvasElement, container: Element): Viewer {
+        const reprMap = new Map<StructureRepresentation, Set<RenderObject>>()
+
         const input = InputObserver.create(canvas)
         input.resize.subscribe(handleResize)
 
@@ -82,13 +89,33 @@ namespace Viewer {
         handleResize()
 
         return {
-            add: (o: RenderObject) => {
-                renderer.add(o)
+            hide: (repr: StructureRepresentation) => {
+                const renderObjectSet = reprMap.get(repr)
+                if (renderObjectSet) renderObjectSet.forEach(o => o.visible = false)
+            },
+            show: (repr: StructureRepresentation) => {
+                const renderObjectSet = reprMap.get(repr)
+                if (renderObjectSet) renderObjectSet.forEach(o => o.visible = true)
+            },
+
+            add: (repr: StructureRepresentation) => {
+                const oldRO = reprMap.get(repr)
+                const newRO = new Set<RenderObject>()
+                repr.renderObjects.forEach(o => newRO.add(o))
+                if (oldRO) {
+                    SetUtils.difference(newRO, oldRO).forEach(o => renderer.add(o))
+                    SetUtils.difference(oldRO, newRO).forEach(o => renderer.remove(o))
+                } else {
+                    repr.renderObjects.forEach(o => renderer.add(o))
+                }
+                reprMap.set(repr, newRO)
             },
-            remove: (o: RenderObject) => {
-                renderer.remove(o)
+            remove: (repr: StructureRepresentation) => {
+                const renderObjectSet = reprMap.get(repr)
+                if (renderObjectSet) renderObjectSet.forEach(o => renderer.remove(o))
             },
             clear: () => {
+                reprMap.clear()
                 renderer.clear()
             },