From 730337aca5ecbc24b66db0933b72c31a8ad9c67e Mon Sep 17 00:00:00 2001
From: Alexander Rose <alex.rose@rcsb.org>
Date: Wed, 11 Apr 2018 18:25:43 -0700
Subject: [PATCH] wip, react components

---
 package-lock.json                             | Bin 424340 -> 441292 bytes
 package.json                                  |   4 +-
 .../render-test/components/file-input.tsx     |  40 ++++
 src/apps/render-test/components/observer.tsx  |  21 ++
 src/apps/render-test/components/viewport.tsx  |  22 ++
 src/apps/render-test/index.tsx                |   2 +-
 src/apps/render-test/state.ts                 | 225 +++++++++---------
 src/apps/render-test/ui.tsx                   |  75 +++++-
 src/apps/render-test/utils/index.ts           |  27 +++
 src/apps/render-test/{ => utils}/mcubes.ts    |   0
 src/mol-gl/camera.ts                          |   5 +-
 src/mol-gl/renderable/mesh.ts                 |   1 -
 src/mol-gl/renderable/point.ts                |   1 -
 src/mol-gl/renderer.ts                        |  57 +++--
 14 files changed, 339 insertions(+), 141 deletions(-)
 create mode 100644 src/apps/render-test/components/file-input.tsx
 create mode 100644 src/apps/render-test/components/observer.tsx
 create mode 100644 src/apps/render-test/components/viewport.tsx
 create mode 100644 src/apps/render-test/utils/index.ts
 rename src/apps/render-test/{ => utils}/mcubes.ts (100%)

diff --git a/package-lock.json b/package-lock.json
index adc4e46e5fb1e29dd1bde04c49b8f6cce5f3e0de..4ce0ce891139fdffd9528576f7e5180db3a8fb39 100644
GIT binary patch
delta 9207
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

delta 488
zcmX@}U24i_$ql!8HyiOMvP?fWgH3btZ9c)xyM(7PZr&pnzF|7=Ay&=l2QD&lO`nj>
zCb-$<IEUP3n`fm8+pF0bnOU}b@G#m5Pj6qqEIc`HBhT~+=a^ZhADGByw0))mV~W}2
z6c>f<T;Ys!c(*S~Wn95Ded<I;h3(-Pj7xd8e=TLK<K3>+%4l_Cdt^M5Md<e2txUYE
z(>>alMW+AFVnH_S#~!8*Hb#r-jfa_1r@y|#%)7npC{wEB_Br>N9y3mt+{7w4`5haQ
zhUvZwnZ&lQf5gPdF+I<aQEU4LVHQos?KfXDh39Xdp2M8W$!M{iwTyW_>vUfmCX4MC
zCNuwF+-_daEGYwaa_nqo6Oru?wllAn+}?MISwesNVJ4O{e%pP5SQ?bJuP<e>5Zu0^
zpM_Uo`v0XY?Az}xW)Xt=?e9{SNvh1IdZv>bM2)Bat!5UOd`M6^)Tzq6Fu&A1%+WL7
zJu^MWD=MifDI(FNtRTYFDK{j<&&VZ9zc|%QJGk7@rKH>>I5VTr$urY6(L5|SB*M$0
zpfazl&>+*T$ke^UGR&v4GS#rivts(g%S>j|*Y0K#nas~Gv;FW{76-$v8m#*9+c^_h
TZ#ZwyU&_iAw>_JMEpa;lZA7<H

diff --git a/package.json b/package.json
index a663aa7ad..70f4cd415 100644
--- a/package.json
+++ b/package.json
@@ -80,8 +80,10 @@
   "dependencies": {
     "argparse": "^1.0.10",
     "express": "^4.16.3",
+    "material-ui": "^1.0.0-beta.41",
     "node-fetch": "^2.1.2",
     "react": "^16.3.1",
-    "react-dom": "^16.3.1"
+    "react-dom": "^16.3.1",
+    "rxjs": "^6.0.0-beta.4"
   }
 }
diff --git a/src/apps/render-test/components/file-input.tsx b/src/apps/render-test/components/file-input.tsx
new file mode 100644
index 000000000..f83d200ab
--- /dev/null
+++ b/src/apps/render-test/components/file-input.tsx
@@ -0,0 +1,40 @@
+/**
+ * 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 TextField from 'material-ui/TextField';
+// import FileUpload from '@material-ui/icons/FileUpload';
+
+import State from '../state'
+import Observer from './observer';
+
+export default class FileInput extends Observer<{ state: State } & WithStyles, { loading: boolean }> {
+    state = { loading: false }
+
+    componentDidMount() {
+        this.subscribe(this.props.state.loading, value => {
+           this.setState({ loading: value });
+        });
+    }
+
+    render() {
+        const { classes, state } = this.props;
+
+        return <TextField
+            label='PDB ID'
+            className={classes.textField}
+            disabled={this.state.loading}
+            margin='normal'
+            onChange={(event) => {
+                state.pdbId = event.target.value
+            }}
+            onKeyPress={(event) => {
+                if (event.key === 'Enter') state.loadPdbId()
+            }}
+        />
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/components/observer.tsx b/src/apps/render-test/components/observer.tsx
new file mode 100644
index 000000000..67e48fb23
--- /dev/null
+++ b/src/apps/render-test/components/observer.tsx
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { Observable, Subscription } from 'rxjs';
+
+export default class Observer<S, P> extends React.Component<S, P> {
+    private _subs: Subscription[] = []
+
+    subscribe<T>(obs: Observable<T>, onNext: (v: T) => void) {
+        this._subs.push(obs.subscribe(onNext));
+    }
+
+    componentWillUnmount() {
+        for (const s of this._subs) s.unsubscribe();
+        this._subs = [];
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/components/viewport.tsx b/src/apps/render-test/components/viewport.tsx
new file mode 100644
index 000000000..1536cd5a4
--- /dev/null
+++ b/src/apps/render-test/components/viewport.tsx
@@ -0,0 +1,22 @@
+/**
+ * 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 State from '../state'
+
+export default class Viewport extends React.Component<{ state: State }, { initialized: boolean }> {
+    private canvasContainer: HTMLDivElement | null = null;
+    state = { initialized: false }
+
+    componentDidMount() {
+        if (this.canvasContainer) this.props.state.initRenderer(this.canvasContainer).then(() => this.setState({ initialized: true }))
+    }
+
+    render() {
+        return <div ref={elm => this.canvasContainer = elm} style={{ height: '100%' }}>
+        </div>
+    }
+}
\ No newline at end of file
diff --git a/src/apps/render-test/index.tsx b/src/apps/render-test/index.tsx
index 246502b30..2e87d291f 100644
--- a/src/apps/render-test/index.tsx
+++ b/src/apps/render-test/index.tsx
@@ -10,4 +10,4 @@ import * as React from 'react'
 import * as ReactDOM from 'react-dom'
 
 const state = new State()
-ReactDOM.render(<UI state={state} />, document.getElementById('app'));
\ No newline at end of file
+ReactDOM.render(<UI state={ state } />, document.getElementById('app'));
\ No newline at end of file
diff --git a/src/apps/render-test/state.ts b/src/apps/render-test/state.ts
index bfc4e2a2a..abb3245e4 100644
--- a/src/apps/render-test/state.ts
+++ b/src/apps/render-test/state.ts
@@ -4,127 +4,47 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
+import { BehaviorSubject } from 'rxjs';
 
-import { Vec3, Mat4 } from 'mol-math/linear-algebra'
-import { createRenderer, createRenderObject } from 'mol-gl/renderer'
-import { createColorTexture } from 'mol-gl/util';
+// import { ValueCell } from 'mol-util/value-cell'
+
+// import { Vec3, Mat4 } from 'mol-math/linear-algebra'
+import { createRenderer, Renderer } from 'mol-gl/renderer'
+// import { createColorTexture } from 'mol-gl/util';
 // import Icosahedron from 'mol-geo/primitive/icosahedron'
 // import Box from 'mol-geo/primitive/box'
 import Spacefill from 'mol-geo/representation/structure/spacefill'
 import Point from 'mol-geo/representation/structure/point'
 
-import CIF from 'mol-io/reader/cif'
-import { Run, Progress } from 'mol-task'
-import { Structure, Symmetry } from 'mol-model/structure'
-
-function log(progress: Progress) {
-    const p = progress.root.progress
-    console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
-}
-
-async function parseCif(data: string|Uint8Array) {
-    const comp = CIF.parse(data)
-    const parsed = await Run(comp, log, 100);
-    if (parsed.isError) throw parsed;
-    return parsed
-}
-
-async function getPdb(pdb: string) {
-    const data = await fetch(`https://files.rcsb.org/download/${pdb}.cif`)
-    const parsed = await parseCif(await data.text())
-    const structure = Structure.ofData({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
-    return structure
-}
+import { Run } from 'mol-task'
+import { Symmetry } from 'mol-model/structure'
 
-import mcubes from './mcubes'
+// import mcubes from './utils/mcubes'
+import { getStructuresFromPdbId } from './utils'
 import { StructureRepresentation } from 'mol-geo/representation/structure';
 // import Cylinder from 'mol-geo/primitive/cylinder';
 
 export default class State {
+    renderer: Renderer
+    pdbId = '1crn'
+    initialized = new BehaviorSubject<boolean>(false)
+    loading = new BehaviorSubject<boolean>(false)
+
     async initRenderer (container: HTMLDivElement) {
-        const renderer = createRenderer(container)
-
-        const p1 = Vec3.create(0, 4, 0)
-        const p2 = Vec3.create(-3, 0, 0)
-
-        // const position = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]))
-        // const normal = ValueCell.create(new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]))
-
-        const transformArray1 = ValueCell.create(new Float32Array(16))
-        const transformArray2 = ValueCell.create(new Float32Array(16 * 3))
-        const m4 = Mat4.identity()
-        Mat4.toArray(m4, transformArray1.ref.value, 0)
-        Mat4.toArray(m4, transformArray2.ref.value, 0)
-        Mat4.setTranslation(m4, p1)
-        Mat4.toArray(m4, transformArray2.ref.value, 16)
-        Mat4.setTranslation(m4, p2)
-        Mat4.toArray(m4, transformArray2.ref.value, 32)
-
-        const color = ValueCell.create(createColorTexture(3))
-        color.ref.value.set([
-            0, 0, 255,
-            0, 255, 0,
-            255, 0, 0
-        ])
-
-        // const points = createRenderObject('point', {
-        //     position,
-        //     transform: transformArray1
-        // })
-        // // renderer.add(points)
-
-        // const mesh = createRenderObject('mesh', {
-        //     position,
-        //     normal,
-        //     color,
-        //     transform: transformArray2
-        // })
-        // renderer.add(mesh)
-
-        // const cylinder = Cylinder({ height: 3, radiusBottom: 0.5, radiusTop: 0.5 })
-        // console.log(cylinder)
-        // const cylinderMesh = createRenderObject('mesh', {
-        //     position: ValueCell.create(cylinder.vertices),
-        //     normal: ValueCell.create(cylinder.normals),
-        //     color,
-        //     transform: transformArray2
-        // }, cylinder.indices)
-        // renderer.add(cylinderMesh)
-
-        // const sphere = Icosahedron()
-        // console.log(sphere)
-
-        // const box = Box()
-        // console.log(box)
-
-        // const points2 = createRenderObject('point', {
-        //     position: ValueCell.create(new Float32Array(box.vertices)),
-        //     transform: transformArray1
-        // })
-        // renderer.add(points2)
-
-        let rr = 0.7;
-        function cubesF(x: number, y: number, z: number) {
-            return x * x + y * y + z * z - rr * rr;
-        }
-        let cubes = await mcubes(cubesF);
-
-        const makeCubesMesh = () => createRenderObject('mesh', {
-            position: cubes.surface.vertexBuffer,
-            normal: cubes.surface.normalBuffer,
-            color,
-            transform: transformArray2,
-            elements: cubes.surface.indexBuffer,
-
-            instanceCount: transformArray2.ref.value.length / 16,
-            elementCount: cubes.surface.triangleCount,
-            positionCount: cubes.surface.vertexCount
-        }, {});
-        const mesh2 = makeCubesMesh();
-        renderer.add(mesh2)
-
-        const structures = await getPdb('1rb8')
+        this.renderer = createRenderer(container)
+        this.initialized.next(true)
+        this.loadPdbId()
+        this.renderer.frame()
+    }
+
+    async loadPdbId () {
+        const { renderer, pdbId } = this
+        renderer.clear()
+
+        if (pdbId.length !== 4) return
+        this.loading.next(true)
+
+        const structures = await getStructuresFromPdbId(pdbId)
         const struct = Symmetry.buildAssembly(structures[0], '1')
 
         const structPointRepr = StructureRepresentation(Point)
@@ -135,6 +55,91 @@ export default class State {
         await Run(structSpacefillRepr.create(struct))
         structSpacefillRepr.renderObjects.forEach(renderer.add)
 
-        renderer.frame()
+        renderer.draw(true)
+
+        this.loading.next(false)
     }
 }
+
+
+
+// async foo () {
+//     const p1 = Vec3.create(0, 4, 0)
+//     const p2 = Vec3.create(-3, 0, 0)
+
+//     // const position = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]))
+//     // const normal = ValueCell.create(new Float32Array([0, 0, 0, 0, 0, 0, 0, 0, 0]))
+
+//     const transformArray1 = ValueCell.create(new Float32Array(16))
+//     const transformArray2 = ValueCell.create(new Float32Array(16 * 3))
+//     const m4 = Mat4.identity()
+//     Mat4.toArray(m4, transformArray1.ref.value, 0)
+//     Mat4.toArray(m4, transformArray2.ref.value, 0)
+//     Mat4.setTranslation(m4, p1)
+//     Mat4.toArray(m4, transformArray2.ref.value, 16)
+//     Mat4.setTranslation(m4, p2)
+//     Mat4.toArray(m4, transformArray2.ref.value, 32)
+
+//     const color = ValueCell.create(createColorTexture(3))
+//     color.ref.value.set([
+//         0, 0, 255,
+//         0, 255, 0,
+//         255, 0, 0
+//     ])
+
+//     // const points = createRenderObject('point', {
+//     //     position,
+//     //     transform: transformArray1
+//     // })
+//     // // renderer.add(points)
+
+//     // const mesh = createRenderObject('mesh', {
+//     //     position,
+//     //     normal,
+//     //     color,
+//     //     transform: transformArray2
+//     // })
+//     // renderer.add(mesh)
+
+//     // const cylinder = Cylinder({ height: 3, radiusBottom: 0.5, radiusTop: 0.5 })
+//     // console.log(cylinder)
+//     // const cylinderMesh = createRenderObject('mesh', {
+//     //     position: ValueCell.create(cylinder.vertices),
+//     //     normal: ValueCell.create(cylinder.normals),
+//     //     color,
+//     //     transform: transformArray2
+//     // }, cylinder.indices)
+//     // renderer.add(cylinderMesh)
+
+//     // const sphere = Icosahedron()
+//     // console.log(sphere)
+
+//     // const box = Box()
+//     // console.log(box)
+
+//     // const points2 = createRenderObject('point', {
+//     //     position: ValueCell.create(new Float32Array(box.vertices)),
+//     //     transform: transformArray1
+//     // })
+//     // renderer.add(points2)
+
+//     // let rr = 0.7;
+//     // function cubesF(x: number, y: number, z: number) {
+//     //     return x * x + y * y + z * z - rr * rr;
+//     // }
+//     // let cubes = await mcubes(cubesF);
+
+//     // const makeCubesMesh = () => createRenderObject('mesh', {
+//     //     position: cubes.surface.vertexBuffer,
+//     //     normal: cubes.surface.normalBuffer,
+//     //     color,
+//     //     transform: transformArray2,
+//     //     elements: cubes.surface.indexBuffer,
+
+//     //     instanceCount: transformArray2.ref.value.length / 16,
+//     //     elementCount: cubes.surface.triangleCount,
+//     //     positionCount: cubes.surface.vertexCount
+//     // }, {});
+//     // const mesh2 = makeCubesMesh();
+//     // renderer.add(mesh2)
+// }
\ No newline at end of file
diff --git a/src/apps/render-test/ui.tsx b/src/apps/render-test/ui.tsx
index dbdf41f3b..e0ca7ec60 100644
--- a/src/apps/render-test/ui.tsx
+++ b/src/apps/render-test/ui.tsx
@@ -5,18 +5,75 @@
  */
 
 import * as React from 'react'
+import { withStyles, WithStyles, Theme, StyleRulesCallback } from 'material-ui/styles';
+import Typography from 'material-ui/Typography';
+import Toolbar from 'material-ui/Toolbar';
+import AppBar from 'material-ui/AppBar';
+import Drawer from 'material-ui/Drawer';
+
+import Viewport from './components/viewport'
+import FileInput from './components/file-input'
 import State from './state'
 
-export default class Root extends React.Component<{ state: State }, { initialized: boolean }> {
-    private canvasContainer: HTMLDivElement | null = null;
-    state = { initialized: false }
+const styles: StyleRulesCallback<any> = (theme: Theme) => ({
+    root: {
+        flexGrow: 1,
+        height: 830,
+        zIndex: 1,
+        overflow: 'hidden',
+        position: 'relative',
+        display: 'flex',
+    },
+    appBar: {
+        zIndex: theme.zIndex.drawer + 1,
+    },
+    drawerPaper: {
+        position: 'relative',
+        width: 240,
+    },
+    content: {
+        flexGrow: 1,
+        backgroundColor: theme.palette.background.default,
+        padding: theme.spacing.unit * 3,
+        minWidth: 0, // So the Typography noWrap works
+    },
+    toolbar: theme.mixins.toolbar,
+    textField: {
+        marginLeft: theme.spacing.unit,
+        marginRight: theme.spacing.unit,
+        width: 200,
+    },
+});
 
-    componentDidMount() {
-        if (this.canvasContainer) this.props.state.initRenderer(this.canvasContainer).then(() => this.setState({ initialized: true }))
-    }
+const decorate = withStyles(styles);
 
+interface Props {
+    state: State;
+};
+
+class UI extends React.Component<{ state: State } & WithStyles, {  }> {
     render() {
-        return <div ref={elm => this.canvasContainer = elm} style={{ position: 'absolute', top: 0, right: 0, left: 0, bottom: 0, overflow: 'hidden' }}>
-        </div>
+        const { classes, state } = this.props;
+        return (
+            <div className={classes.root}>
+                <AppBar position='absolute' className={classes.appBar}>
+                    <Toolbar>
+                    <Typography variant='title' color='inherit' noWrap>
+                        Mol* Render Test
+                    </Typography>
+                    </Toolbar>
+                </AppBar>
+                <Drawer variant='permanent' classes={{ paper: classes.drawerPaper, }}>
+                    <div className={classes.toolbar} />
+                    <FileInput state={state} classes={classes}></FileInput>
+                </Drawer>
+                <main className={classes.content}>
+                    <div className={classes.toolbar} />
+                    <Viewport state={state}></Viewport>
+                </main>
+            </div>
+        );
     }
-}
\ No newline at end of file
+}
+
+export default decorate<Props>(UI)
\ No newline at end of file
diff --git a/src/apps/render-test/utils/index.ts b/src/apps/render-test/utils/index.ts
new file mode 100644
index 000000000..67a7eb9a3
--- /dev/null
+++ b/src/apps/render-test/utils/index.ts
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import CIF from 'mol-io/reader/cif'
+import { Run, Progress } from 'mol-task'
+import { Structure } from 'mol-model/structure'
+
+export function log(progress: Progress) {
+    const p = progress.root.progress
+    console.log(`${p.message} ${(p.current/p.max*100).toFixed(2)}%`)
+}
+
+export async function parseCif(data: string|Uint8Array) {
+    const comp = CIF.parse(data)
+    const parsed = await Run(comp, log, 100);
+    if (parsed.isError) throw parsed;
+    return parsed
+}
+
+export async function getStructuresFromPdbId(pdbid: string) {
+    const data = await fetch(`https://files.rcsb.org/download/${pdbid}.cif`)
+    const parsed = await parseCif(await data.text())
+    return Structure.ofData({ kind: 'mmCIF', data: CIF.schema.mmCIF(parsed.result.blocks[0]) })
+}
\ No newline at end of file
diff --git a/src/apps/render-test/mcubes.ts b/src/apps/render-test/utils/mcubes.ts
similarity index 100%
rename from src/apps/render-test/mcubes.ts
rename to src/apps/render-test/utils/mcubes.ts
diff --git a/src/mol-gl/camera.ts b/src/mol-gl/camera.ts
index f3a3ffa04..623e24672 100644
--- a/src/mol-gl/camera.ts
+++ b/src/mol-gl/camera.ts
@@ -49,7 +49,7 @@ export interface Camera {
     update: (props: any, block: any) => void,
     setState: (newState: CameraState) => void,
     getState: () => CameraState,
-    isDirty: () => boolean
+    dirty: boolean
 }
 
 export namespace Camera {
@@ -185,7 +185,8 @@ export namespace Camera {
             update,
             setState,
             getState: () => Object.assign({}, state),
-            isDirty: () => dirty
+            get dirty() { return dirty },
+            set dirty(value: boolean) { dirty = value }
         }
     }
 }
diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts
index a7fa373f9..52c2244c7 100644
--- a/src/mol-gl/renderable/mesh.ts
+++ b/src/mol-gl/renderable/mesh.ts
@@ -31,7 +31,6 @@ namespace Mesh {
     }
 
     export function create(regl: REGL.Regl, data: Data, uniforms: Uniforms): Renderable {
-        console.log(data)
         const instanceId = ValueCell.create(fillSerial(new Float32Array(data.instanceCount)))
         const command = regl({
             ...MeshShaders,
diff --git a/src/mol-gl/renderable/point.ts b/src/mol-gl/renderable/point.ts
index 99b311655..5bc8aed30 100644
--- a/src/mol-gl/renderable/point.ts
+++ b/src/mol-gl/renderable/point.ts
@@ -24,7 +24,6 @@ namespace Point {
     }
 
     export function create(regl: REGL.Regl, data: Data): Renderable {
-        console.log(data)
         const instanceId = ValueCell.create(fillSerial(new Float32Array(data.instanceCount)))
         const command = regl({
             ...PointShaders,
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index f4c25f0f6..eef94cf27 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -39,7 +39,8 @@ export function createRenderObject(type: 'mesh' | 'point', data: PointRenderable
 export interface Renderer {
     add: (o: RenderObject) => void
     remove: (o: RenderObject) => void
-    draw: () => void
+    clear: () => void
+    draw: (force: boolean) => void
     frame: () => void
 }
 
@@ -54,18 +55,33 @@ export function createRenderer(container: HTMLDivElement): Renderer {
     const renderableList: Renderable[] = []
     const objectIdRenderableMap: { [k: number]: Renderable } = {}
 
-    const regl = glContext.create({
-        container,
-        extensions: [
-            'OES_texture_float',
-            'OES_texture_float_linear',
-            'OES_element_index_uint',
-            // 'EXT_disjoint_timer_query',
-            'EXT_blend_minmax',
-            'ANGLE_instanced_arrays'
-        ],
-        profile: true
-    })
+    let regl: REGL.Regl
+    try {
+        regl = glContext.create({
+            container,
+            extensions: [
+                'OES_texture_float',
+                'OES_texture_float_linear',
+                'OES_element_index_uint',
+                'EXT_disjoint_timer_query',
+                'EXT_blend_minmax',
+                'ANGLE_instanced_arrays'
+            ],
+            profile: true
+        })
+    } catch (e) {
+        regl = glContext.create({
+            container,
+            extensions: [
+                'OES_texture_float',
+                'OES_texture_float_linear',
+                'OES_element_index_uint',
+                'EXT_blend_minmax',
+                'ANGLE_instanced_arrays'
+            ],
+            profile: true
+        })
+    }
 
     const camera = Camera.create(regl, container, {
         center: Vec3.create(0, 0, 0),
@@ -91,12 +107,12 @@ export function createRenderer(container: HTMLDivElement): Renderer {
         }
     })
 
-    const draw = () => {
+    const draw = (force = false) => {
         camera.update((state: any) => {
-            if (!camera.isDirty()) return;
+            if (!force && !camera.dirty) return;
             baseContext(() => {
                 // console.log(ctx)
-                regl.clear({color: [0, 0, 0, 1]})
+                regl.clear({ color: [0, 0, 0, 1] })
                 // TODO painters sort, filter visible, filter picking, visibility culling?
                 renderableList.forEach(r => {
                     r.draw()
@@ -118,6 +134,15 @@ export function createRenderer(container: HTMLDivElement): Renderer {
                 delete objectIdRenderableMap[o.id]
             }
         },
+        clear: () => {
+            for (const id in objectIdRenderableMap) {
+                // TODO
+                // objectIdRenderableMap[id].destroy()
+                delete objectIdRenderableMap[id]
+            }
+            renderableList.length = 0
+            camera.dirty = true
+        },
         draw,
         frame: () => {
             regl.frame((ctx) => draw())
-- 
GitLab