Browse Source

mol-plugin: 1st prototype

David Sehnal 6 years ago
parent
commit
9a81d6ccac

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

@@ -0,0 +1,33 @@
+<!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* Viewer</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="position: absolute; width: 100%; height: 100%"></div>
+        <script type="text/javascript" src="./index.js"></script>
+    </body>
+</html>

+ 10 - 0
src/apps/viewer/index.ts

@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { createPlugin } from 'mol-plugin';
+import './index.html'
+
+createPlugin(document.getElementById('app')!);

+ 1 - 1
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -33,8 +33,8 @@ export type PolymerTraceMeshProps = typeof DefaultPolymerTraceMeshProps
 
 async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, structure: Structure, props: PolymerTraceMeshProps, mesh?: Mesh) {
     const polymerElementCount = unit.polymerElements.length
-    if (!polymerElementCount) return Mesh.createEmpty(mesh)
 
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
     const sizeTheme = SizeTheme({ name: props.sizeTheme, value: props.sizeValue, factor: props.sizeFactor })
     const { linearSegments, radialSegments, aspectRatio, arrowFactor } = props
 

+ 89 - 4
src/mol-plugin/context.ts

@@ -4,18 +4,103 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State } from 'mol-state';
+import { State, StateTree, StateSelection, Transformer } from 'mol-state';
 import Viewer from 'mol-canvas3d/viewer';
+import { StateTransforms } from './state/transforms';
+import { Subject } from 'rxjs';
+import { PluginStateObjects as SO } from './state/objects';
 
 export class PluginContext {
     state = {
-        data: State,
-        behaviour: State,
-        plugin: State
+        data: State.create(new SO.Root({ label: 'Root' }, { })),
+        // behaviour: State,
+        // plugin: State
+    };
+
+    // TODO: better events
+    events = {
+        stateUpdated: new Subject<undefined>()
     };
 
     viewer: Viewer;
 
+    initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
+        try {
+            this.viewer = Viewer.create(canvas, container);
+            this.viewer.animate();
+            console.log('viewer created');
+            return true;
+        } catch (e) {
+            console.error(e);
+            return false;
+        }
+    }
+
+    _test_createState(url: string) {
+        const b = StateTree.build(this.state.data.tree);
+        const newTree = b.toRoot()
+            .apply(StateTransforms.Data.Download, { url })
+            .apply(StateTransforms.Data.ParseCif)
+            .apply(StateTransforms.Model.CreateModelsFromMmCif, {}, { ref: 'models' })
+            .apply(StateTransforms.Model.CreateStructureFromModel, { modelIndex: 0 }, { ref: 'structure' })
+            .apply(StateTransforms.Visuals.CreateStructureRepresentation)
+            .getTree();
+
+        this._test_updateStateData(newTree);
+    }
+
+    async _test_updateStateData(tree: StateTree) {
+        const newState = await State.update(this.state.data, tree).run(p => console.log(p), 250);
+        this.state.data = newState;
+        console.log(newState);
+        this.events.stateUpdated.next();
+    }
+
+    private initEvents() {
+        this.state.data.context.events.object.created.subscribe(o => {
+            if (!SO.StructureRepresentation3D.is(o.obj)) return;
+            console.log('adding repr', o.obj.data.repr);
+            this.viewer.add(o.obj.data.repr);
+            this.viewer.requestDraw(true);
+        });
+        this.state.data.context.events.object.updated.subscribe(o => {
+            const oo = o.obj;
+            if (!SO.StructureRepresentation3D.is(oo)) return;
+            console.log('adding repr', oo.data.repr);
+            this.viewer.add(oo.data.repr);
+            this.viewer.requestDraw(true);
+        });
+    }
+
+    _test_centerView() {
+        const sel = StateSelection.select('structure', this.state.data);
+        const center = (sel[0].obj! as SO.Structure).data.boundary.sphere.center;
+        console.log({ sel, center, rc: this.viewer.reprCount });
+        this.viewer.center(center);
+        this.viewer.requestDraw(true);
+    }
+
+    _test_nextModel() {
+        const models = StateSelection.select('models', this.state.data)[0].obj as SO.Models;
+        const idx = (this.state.data.tree.getValue('structure')!.params as Transformer.Params<typeof StateTransforms.Model.CreateStructureFromModel>).modelIndex;
+        console.log({ idx });
+        const newTree = StateTree.updateParams(this.state.data.tree, 'structure', { modelIndex: (idx + 1) % models.data.length });
+        return this._test_updateStateData(newTree);
+        // this.viewer.requestDraw(true);
+    }
+
+    _test_playModels() {
+        const update = async () => {
+            await this._test_nextModel();
+            setTimeout(update, 1000 / 15);
+        }
+        update();
+    }
+
+    constructor() {
+        this.initEvents();
+    }
+
     // logger = ;
     // settings = ;
 }

+ 10 - 1
src/mol-plugin/index.ts

@@ -4,4 +4,13 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-// TODO
+import { PluginContext } from './context';
+import { Plugin } from './ui/plugin'
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+
+export function createPlugin(target: HTMLElement): PluginContext {
+    const ctx = new PluginContext();
+    ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target);
+    return ctx;
+}

+ 2 - 2
src/mol-plugin/state/base.ts

@@ -11,9 +11,9 @@ export type TypeClass = 'root' | 'data' | 'prop'
 export namespace PluginStateObject {
     export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behaviour'
     export interface TypeInfo { name: string, shortName: string, description: string, typeClass: TypeClass }
-    export interface PluginStateObjectProps { label: string }
+    export interface Props { label: string, desctiption?: string }
 
-    export const Create = StateObject.factory<TypeInfo, PluginStateObjectProps>();
+    export const Create = StateObject.factory<TypeInfo, Props>();
 }
 
 export namespace PluginStateTransform {

+ 5 - 2
src/mol-plugin/state/types.ts → src/mol-plugin/state/objects.ts

@@ -7,6 +7,7 @@
 import { PluginStateObject } from './base';
 import { CifFile } from 'mol-io/reader/cif';
 import { Model as _Model, Structure as _Structure } from 'mol-model/structure'
+import { StructureRepresentation } from 'mol-geo/representation/structure';
 
 const _create = PluginStateObject.Create
 
@@ -26,12 +27,14 @@ namespace PluginStateObjects {
         // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { }
     }
 
-    export class Model extends _create<_Model>({ name: 'Molecule Model', typeClass: 'Object', shortName: 'M_M', description: 'A model of a molecule.' }) { }
+    export class Models extends _create<ReadonlyArray<_Model>>({ name: 'Molecule Model', typeClass: 'Object', shortName: 'M_M', description: 'A model of a molecule.' }) { }
     export class Structure extends _create<_Structure>({ name: 'Molecule Structure', typeClass: 'Object', shortName: 'M_S', description: 'A structure of a molecule.' }) { }
 
 
-    export class StructureRepresentation extends _create<{
+    export class StructureRepresentation3D extends _create<{
+        repr: StructureRepresentation<any>,
         // TODO
+        // props
     }>({ name: 'Molecule Structure Representation', typeClass: 'Representation', shortName: 'S_R', description: 'A representation of a molecular structure.' }) { }
 }
 

+ 15 - 0
src/mol-plugin/state/transforms.ts

@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as Data from './transforms/data'
+import * as Model from './transforms/model'
+import * as Visuals from './transforms/visuals'
+
+export const StateTransforms = {
+    Data,
+    Model,
+    Visuals
+}

+ 38 - 0
src/mol-plugin/state/transforms/data.ts

@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateTransform } from '../base';
+import { PluginStateObjects as SO } from '../objects';
+import { Task } from 'mol-task';
+import CIF from 'mol-io/reader/cif'
+
+export const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, { url: string, isBinary?: boolean, label?: string }>({
+    name: 'download',
+    from: [SO.Root],
+    to: [SO.Data.String, SO.Data.Binary],
+    apply({ params: p }) {
+        return Task.create('Download', async ctx => {
+            // TODO: track progress
+            const req = await fetch(p.url);
+            return p.isBinary
+                ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, new Uint8Array(await req.arrayBuffer()))
+                : new SO.Data.String({ label: p.label ? p.label : p.url }, await req.text());
+        });
+    }
+});
+
+export const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, { }>({
+    name: 'parse-cif',
+    from: [SO.Data.String, SO.Data.Binary],
+    to: [SO.Data.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({ label: 'CIF File' }, parsed.result);
+        });
+    }
+});

+ 41 - 0
src/mol-plugin/state/transforms/model.ts

@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateTransform } from '../base';
+import { PluginStateObjects as SO } from '../objects';
+import { Task } from 'mol-task';
+import { Model, Format, Structure } from 'mol-model/structure';
+
+export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, { blockHeader?: string }>({
+    name: 'create-models-from-mmcif',
+    from: [SO.Data.Cif],
+    to: [SO.Models],
+    defaultParams: a => ({ blockHeader: a.data.blocks[0].header }),
+    apply({ a, params }) {
+        return Task.create('Parse mmCIF', async ctx => {
+            const header = params.blockHeader || a.data.blocks[0].header;
+            const block = a.data.blocks.find(b => b.header === header);
+            if (!block) throw new Error(`Data block '${[header]}' not found.`);
+            const models = await Model.create(Format.mmCIF(block)).runInContext(ctx);
+            if (models.length === 0) throw new Error('No models found.');
+            const label = models.length === 1 ? `${models[0].label}` : `${models[0].label} (${models.length} models)`;
+            return new SO.Models({ label }, models);
+        });
+    }
+});
+
+export const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, { modelIndex: number }>({
+    name: 'structure-from-model',
+    from: [SO.Models],
+    to: [SO.Structure],
+    defaultParams: () => ({ modelIndex: 0 }),
+    apply({ a, params }) {
+        if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
+        // TODO: make Structure.ofModel async?
+        const s = Structure.ofModel(a.data[params.modelIndex]);
+        return new SO.Structure({ label: `${a.data[params.modelIndex].label} (model ${s.models[0].modelNum})`, desctiption: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s);
+    }
+});

+ 31 - 0
src/mol-plugin/state/transforms/visuals.ts

@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CartoonRepresentation, DefaultCartoonProps } from 'mol-geo/representation/structure/representation/cartoon';
+import { Transformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { PluginStateTransform } from '../base';
+import { PluginStateObjects as SO } from '../objects';
+
+export const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, { }>({
+    name: 'create-structure-representation',
+    from: [SO.Structure],
+    to: [SO.StructureRepresentation3D],
+    defaultParams: () => ({ modelIndex: 0 }),
+    apply({ a, params }) {
+        return Task.create('Structure Representation', async ctx => {
+            const repr = CartoonRepresentation();
+            await repr.createOrUpdate({ ...DefaultCartoonProps }, a.data).runInContext(ctx);
+            return new SO.StructureRepresentation3D({ label: 'Cartoon' }, { repr });
+        });
+    },
+    update({ a, b }) {
+        return Task.create('Structure Representation', async ctx => {
+            await b.data.repr.createOrUpdate(b.data.repr.props, a.data).runInContext(ctx);
+            return Transformer.UpdateResult.Updated;
+        });
+    }
+});

+ 28 - 0
src/mol-plugin/ui/controls.tsx

@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginContext } from '../context';
+
+export class Controls extends React.Component<{ plugin: PluginContext }, { id: string }> {
+    state = { id: '1grm' };
+
+    private createState = () => {
+        const url = `http://www.ebi.ac.uk/pdbe/static/entry/${this.state.id.toLowerCase()}_updated.cif`;
+        // const url = `https://webchem.ncbr.muni.cz/CoordinateServer/${this.state.id.toLowerCase()}/full`
+        this.props.plugin._test_createState(url);
+    }
+
+    render() {
+        return <div>
+            <input type='text' defaultValue={this.state.id} onChange={e => this.setState({ id: e.currentTarget.value })} />
+            <button onClick={this.createState}>Create State</button><br/>
+            <button onClick={() => this.props.plugin._test_centerView()}>Center View</button><br/>
+            <button onClick={() => this.props.plugin._test_nextModel()}>Next Model</button><br/>
+            <button onClick={() => this.props.plugin._test_playModels()}>Play Models</button><br/>
+        </div>;
+    }
+}

+ 27 - 0
src/mol-plugin/ui/plugin.tsx

@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginContext } from '../context';
+import { Tree } from './tree';
+import { Viewport } from './viewport';
+import { Controls } from './controls';
+
+export class Plugin extends React.Component<{ plugin: PluginContext }, { }> {
+    render() {
+        return <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
+            <div style={{ position: 'absolute', width: '250px', height: '100%' }}>
+                <Tree plugin={this.props.plugin} />
+            </div>
+            <div style={{ position: 'absolute', left: '250px', right: '250px', height: '100%' }}>
+                <Viewport plugin={this.props.plugin} />
+            </div>
+            <div style={{ position: 'absolute', width: '250px', right: '0', height: '100%' }}>
+                <Controls plugin={this.props.plugin} />
+            </div>
+        </div>;
+    }
+}

+ 36 - 0
src/mol-plugin/ui/tree.tsx

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginContext } from '../context';
+import { PluginStateObject } from 'mol-plugin/state/base';
+
+export class Tree extends React.Component<{ plugin: PluginContext }, { }> {
+
+    componentWillMount() {
+        this.props.plugin.events.stateUpdated.subscribe(() => this.forceUpdate());
+    }
+    render() {
+        const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
+        return <div>
+            {n.children.map(c => <TreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}
+        </div>;
+    }
+}
+
+export class TreeNode extends React.Component<{ plugin: PluginContext, nodeRef: string }, { }> {
+    render() {
+        const n = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!;
+        const obj = this.props.plugin.state.data.objects.get(this.props.nodeRef)!;
+        return <div style={{ borderLeft: '1px solid black', paddingLeft: '5px' }}>
+            {(obj.obj!.props as PluginStateObject.Props).label}
+            {n.children.size === 0
+                ? void 0
+                : <div style={{ marginLeft: '10px' }}>{n.children.map(c => <TreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}</div>
+            }
+        </div>;
+    }
+}

+ 82 - 0
src/mol-plugin/ui/viewport.tsx

@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginContext } from '../context';
+import { Loci, EmptyLoci, areLociEqual } from 'mol-model/loci';
+import { MarkerAction } from 'mol-geo/geometry/marker-data';
+
+interface ViewportProps {
+    plugin: PluginContext
+}
+
+interface ViewportState {
+    noWebGl: boolean
+}
+
+export class Viewport extends React.Component<ViewportProps, ViewportState> {
+    private container: HTMLDivElement | null = null;
+    private canvas: HTMLCanvasElement | null = null;
+
+    state: ViewportState = {
+        noWebGl: false
+    };
+
+    handleResize() {
+        this.props.plugin.viewer.handleResize();
+    }
+
+    componentDidMount() {
+        if (!this.canvas || !this.container || !this.props.plugin.initViewer(this.canvas, this.container)) {
+            this.setState({ noWebGl: true });
+        }
+        this.handleResize();
+
+        const viewer = this.props.plugin.viewer;
+        viewer.input.resize.subscribe(() => this.handleResize());
+
+        let prevLoci: Loci = EmptyLoci;
+        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) return;
+            const p = viewer.identify(x, y);
+            if (p) {
+                const loci = viewer.getLoci(p);
+
+                if (!areLociEqual(loci, prevLoci)) {
+                    viewer.mark(prevLoci, MarkerAction.RemoveHighlight);
+                    viewer.mark(loci, MarkerAction.Highlight);
+                    prevLoci = loci;
+                }
+            }
+        })
+    }
+
+    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>;
+    }
+}

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

@@ -36,10 +36,13 @@ export namespace StateObject {
         return <D = { }, P = {}>(typeInfo: TypeInfo) => create<P & CommonProps, D, TypeInfo>(typeInfo);
     }
 
+    export type Ctor = { new(...args: any[]): StateObject, type: Type }
+
     export function create<Props, Data, TypeInfo>(typeInfo: TypeInfo) {
         const dataType: Type<TypeInfo> = { info: typeInfo };
         return class implements StateObject<Props, Data> {
             static type = dataType;
+            static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; }
             id = UUID.create();
             type = dataType;
             ref = 'not set' as Transform.Ref;