diff --git a/package-lock.json b/package-lock.json index adc4e46e5fb1e29dd1bde04c49b8f6cce5f3e0de..4ce0ce891139fdffd9528576f7e5180db3a8fb39 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index a663aa7ad1187a1a7f40d4496ebd70f56fa34fca..70f4cd4152c0ebca7bd4ee3eea6456d08e04ca97 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 0000000000000000000000000000000000000000..f83d200ab54414550b912eade40e726ce625d7e3 --- /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 0000000000000000000000000000000000000000..67e48fb23485513daf57f4b7f57fc9bba76bd26a --- /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 0000000000000000000000000000000000000000..1536cd5a41f75f1efce8035cd2258296b05b34d5 --- /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 246502b305987115ab9d1930e4e73ecc42ea8876..2e87d291faecababed3fca35c7a2799742704ce9 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 bfc4e2a2a2f2fb61a2b214224964869b0c176a06..abb3245e41f5c80ba25b9d5d7245134c10ece93f 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 dbdf41f3b72e11bacaf22bf4055e17d29b2ffc4d..e0ca7ec6086096cdf968bade7862f2c1a119615e 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 0000000000000000000000000000000000000000..67a7eb9a307501f67f245856ee0c58a5e4a8716a --- /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 f3a3ffa04848428f9f7c1c5d4600bbe6f1e681e7..623e2467284f005393e2358348a52d82b2fa60f0 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 a7fa373f9e9760c9c702e507752c4f4b42099664..52c2244c735739eda229b4cbbd9e21512de60a8b 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 99b31165599afc448cf0e5bb589a87387c49e2bf..5bc8aed303ec225c9e2abb893285e045c9f74a98 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 f4c25f0f64248e9b651e9496759106d08ec9113a..eef94cf27441e2c057a7e721e79e05ca342c14ca 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())