Skip to content
Snippets Groups Projects
Commit 9a81d6cc authored by David Sehnal's avatar David Sehnal
Browse files

mol-plugin: 1st prototype

parent d3949137
No related branches found
No related tags found
No related merge requests found
Showing
with 451 additions and 10 deletions
<!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>
\ No newline at end of file
/**
* 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')!);
\ No newline at end of file
...@@ -33,8 +33,8 @@ export type PolymerTraceMeshProps = typeof DefaultPolymerTraceMeshProps ...@@ -33,8 +33,8 @@ export type PolymerTraceMeshProps = typeof DefaultPolymerTraceMeshProps
async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, structure: Structure, props: PolymerTraceMeshProps, mesh?: Mesh) { async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, structure: Structure, props: PolymerTraceMeshProps, mesh?: Mesh) {
const polymerElementCount = unit.polymerElements.length 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 sizeTheme = SizeTheme({ name: props.sizeTheme, value: props.sizeValue, factor: props.sizeFactor })
const { linearSegments, radialSegments, aspectRatio, arrowFactor } = props const { linearSegments, radialSegments, aspectRatio, arrowFactor } = props
......
...@@ -4,18 +4,103 @@ ...@@ -4,18 +4,103 @@
* @author David Sehnal <david.sehnal@gmail.com> * @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 Viewer from 'mol-canvas3d/viewer';
import { StateTransforms } from './state/transforms';
import { Subject } from 'rxjs';
import { PluginStateObjects as SO } from './state/objects';
export class PluginContext { export class PluginContext {
state = { state = {
data: State, data: State.create(new SO.Root({ label: 'Root' }, { })),
behaviour: State, // behaviour: State,
plugin: State // plugin: State
};
// TODO: better events
events = {
stateUpdated: new Subject<undefined>()
}; };
viewer: Viewer; 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 = ; // logger = ;
// settings = ; // settings = ;
} }
\ No newline at end of file
...@@ -4,4 +4,13 @@ ...@@ -4,4 +4,13 @@
* @author David Sehnal <david.sehnal@gmail.com> * @author David Sehnal <david.sehnal@gmail.com>
*/ */
// TODO import { PluginContext } from './context';
\ No newline at end of file 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;
}
\ No newline at end of file
...@@ -11,9 +11,9 @@ export type TypeClass = 'root' | 'data' | 'prop' ...@@ -11,9 +11,9 @@ export type TypeClass = 'root' | 'data' | 'prop'
export namespace PluginStateObject { export namespace PluginStateObject {
export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behaviour' export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behaviour'
export interface TypeInfo { name: string, shortName: string, description: string, typeClass: TypeClass } 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 { export namespace PluginStateTransform {
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
import { PluginStateObject } from './base'; import { PluginStateObject } from './base';
import { CifFile } from 'mol-io/reader/cif'; import { CifFile } from 'mol-io/reader/cif';
import { Model as _Model, Structure as _Structure } from 'mol-model/structure' import { Model as _Model, Structure as _Structure } from 'mol-model/structure'
import { StructureRepresentation } from 'mol-geo/representation/structure';
const _create = PluginStateObject.Create const _create = PluginStateObject.Create
...@@ -26,12 +27,14 @@ namespace PluginStateObjects { ...@@ -26,12 +27,14 @@ namespace PluginStateObjects {
// }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { } // }>({ 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 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 // TODO
// props
}>({ name: 'Molecule Structure Representation', typeClass: 'Representation', shortName: 'S_R', description: 'A representation of a molecular structure.' }) { } }>({ name: 'Molecule Structure Representation', typeClass: 'Representation', shortName: 'S_R', description: 'A representation of a molecular structure.' }) { }
} }
......
/**
* 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
}
\ No newline at end of file
/**
* 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);
});
}
});
\ No newline at end of file
/**
* 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);
}
});
\ No newline at end of file
/**
* 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;
});
}
});
\ No newline at end of file
/**
* 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>;
}
}
\ No newline at end of file
/**
* 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>;
}
}
\ No newline at end of file
/**
* 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>;
}
}
\ No newline at end of file
/**
* 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>;
}
}
\ No newline at end of file
...@@ -36,10 +36,13 @@ export namespace StateObject { ...@@ -36,10 +36,13 @@ export namespace StateObject {
return <D = { }, P = {}>(typeInfo: TypeInfo) => create<P & CommonProps, D, TypeInfo>(typeInfo); 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) { export function create<Props, Data, TypeInfo>(typeInfo: TypeInfo) {
const dataType: Type<TypeInfo> = { info: typeInfo }; const dataType: Type<TypeInfo> = { info: typeInfo };
return class implements StateObject<Props, Data> { return class implements StateObject<Props, Data> {
static type = dataType; static type = dataType;
static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; }
id = UUID.create(); id = UUID.create();
type = dataType; type = dataType;
ref = 'not set' as Transform.Ref; ref = 'not set' as Transform.Ref;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment