ソースを参照

Merge branch 'master' into gl-lines

Alexander Rose 6 年 前
コミット
6634ac47fd

+ 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
 

+ 1 - 1
src/mol-io/writer/cif/encoder/text.ts

@@ -235,7 +235,7 @@ function writeChecked(builder: StringBuilder, val: string) {
     }
 
     const fst = val.charCodeAt(0);
-    if (!escape && (fst === 35 /* # */ || fst === 59 /* ; */ || hasWhitespace)) {
+    if (!escape && (fst === 35 /* # */|| fst === 36 /* $ */ || fst === 59 /* ; */ || fst === 91 /* [ */ || fst === 93 /* ] */ || hasWhitespace)) {
         escapeCharStart = '\'';
         escapeCharEnd = '\' ';
         escape = true;

+ 0 - 0
src/mol-plugin/behaviour.ts


+ 0 - 0
src/mol-plugin/behaviour/camera.ts


+ 106 - 0
src/mol-plugin/context.ts

@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+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.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-state/tree/selection.ts → src/mol-plugin/spec.ts

@@ -4,8 +4,8 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-namespace StateTreeSelection {
+interface PluginSpec {
 
 }
 
-export { StateTreeSelection }
+export { PluginSpec }

+ 21 - 0
src/mol-plugin/state/base.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateObject, Transformer } from 'mol-state';
+
+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 Props { label: string, desctiption?: string }
+
+    export const Create = StateObject.factory<TypeInfo, Props>();
+}
+
+export namespace PluginStateTransform {
+    export const Create = Transformer.factory('ms-plugin');
+}

+ 41 - 0
src/mol-plugin/state/objects.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 { 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
+
+namespace PluginStateObjects {
+    export class Root extends _create({ name: 'Root', shortName: 'R', typeClass: 'Root', description: 'Where everything begins.' }) { }
+    export class Group extends _create({ name: 'Group', shortName: 'G', typeClass: 'Group', description: 'A group on entities.' }) { }
+
+    export namespace Data {
+        export class String extends _create<string>({ name: 'String Data', typeClass: 'Data', shortName: 'S_D', description: 'A string.' }) { }
+        export class Binary extends _create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data', shortName: 'B_D', description: 'A binary blob.' }) { }
+        export class Json extends _create<any>({ name: 'JSON Data', typeClass: 'Data', shortName: 'JS_D', description: 'Represents JSON data.' }) { }
+        export class Cif extends _create<CifFile>({ name: 'Cif File', typeClass: 'Data', shortName: 'CF', description: 'Represents parsed CIF data.' }) { }
+
+        // TODO
+        // export class MultipleRaw extends _create<{
+        //     [key: string]: { type: 'String' | 'Binary', data: string | Uint8Array }
+        // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { }
+    }
+
+    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 StructureRepresentation3D extends _create<{
+        repr: StructureRepresentation<any>,
+        // TODO
+        // props
+    }>({ name: 'Molecule Structure Representation', typeClass: 'Representation', shortName: 'S_R', description: 'A representation of a molecular structure.' }) { }
+}
+
+export { PluginStateObjects }

+ 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>;
+    }
+}

+ 0 - 0
src/mol-plugin/util/logger.ts


+ 6 - 5
src/mol-state/context.ts

@@ -6,17 +6,18 @@
 
 import { Subject } from 'rxjs'
 import { StateObject } from './object';
-import { Transform } from './tree/transform';
+import { Transform } from './transform';
 
 interface StateContext {
     events: {
         object: {
             stateChanged: Subject<{ ref: Transform.Ref }>,
             propsChanged: Subject<{ ref: Transform.Ref, newProps: unknown }>,
-            updated: Subject<{ ref: Transform.Ref }>,
-            replaced: Subject<{ ref: Transform.Ref, old?: StateObject }>,
-            created: Subject<{ ref: Transform.Ref }>,
-            removed: Subject<{ ref: Transform.Ref }>,
+
+            updated: Subject<{ ref: Transform.Ref, obj?: StateObject }>,
+            replaced: Subject<{ ref: Transform.Ref, oldObj?: StateObject, newObj?: StateObject }>,
+            created: Subject<{ ref: Transform.Ref, obj: StateObject }>,
+            removed: Subject<{ ref: Transform.Ref, obj?: StateObject }>,
         },
         warn: Subject<string>
     },

+ 2 - 2
src/mol-state/index.ts

@@ -9,5 +9,5 @@ export * from './state'
 export * from './transformer'
 export * from './tree'
 export * from './context'
-export * from './tree/transform'
-export * from './tree/selection'
+export * from './transform'
+export * from './selection'

+ 11 - 6
src/mol-state/object.ts

@@ -5,11 +5,12 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Transform } from './tree/transform';
+import { Transform } from './transform';
+import { UUID } from 'mol-util';
 
 /** A mutable state object */
 export interface StateObject<P = unknown, D = unknown> {
-    ref: Transform.Ref,
+    readonly id: UUID,
     readonly type: StateObject.Type,
     readonly props: P,
     readonly data: D
@@ -28,18 +29,21 @@ export namespace StateObject {
     }
 
     export interface Type<Info = any> {
-        kind: string,
         info: Info
     }
 
     export function factory<TypeInfo, CommonProps>() {
-        return <D = { }, P = {}>(kind: string, info: TypeInfo) => create<P & CommonProps, D, TypeInfo>(kind, info);
+        return <D = { }, P = {}>(typeInfo: TypeInfo) => create<P & CommonProps, D, TypeInfo>(typeInfo);
     }
 
-    export function create<Props, Data, TypeInfo>(kind: string, typeInfo: TypeInfo) {
-        const dataType: Type<TypeInfo> = { kind, info: 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;
             constructor(public props: Props, public data: Data) { }
@@ -47,6 +51,7 @@ export namespace StateObject {
     }
 
     export interface Node {
+        ref: Transform.Ref,
         state: StateType,
         props: unknown,
         errorText?: string,

+ 178 - 0
src/mol-state/selection.ts

@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateObject } from './object';
+import { State } from './state';
+import { ImmutableTree } from './util/immutable-tree';
+
+namespace StateSelection {
+    export type Selector = Query | Builder | string | StateObject.Node;
+    export type NodeSeq = StateObject.Node[]
+    export type Query = (state: State) => NodeSeq;
+
+    export function select(s: Selector, state: State) {
+        return compile(s)(state);
+    }
+
+    export function compile(s: Selector): Query {
+        const selector = s ? s : root();
+        let query: Query;
+        if (isBuilder(selector)) query = (selector as any).compile();
+        else if (isObj(selector)) query = (byValue(selector) as any).compile();
+        else if (isQuery(selector)) query = selector;
+        else query = (byRef(selector as string) as any).compile();
+        return query;
+    }
+
+    function isObj(arg: any): arg is StateObject.Node {
+        return (arg as StateObject.Node).version !== void 0;
+    }
+
+    function isBuilder(arg: any): arg is Builder {
+        return arg.compile !== void 0;
+    }
+
+    function isQuery(arg: any): arg is Query {
+        return typeof arg === 'function';
+    }
+
+    export interface Builder {
+        flatMap(f: (n: StateObject.Node) => StateObject.Node[]): Builder;
+        mapEntity(f: (n: StateObject.Node) => StateObject.Node): Builder;
+        unique(): Builder;
+
+        parent(): Builder;
+        first(): Builder;
+        filter(p: (n: StateObject.Node) => boolean): Builder;
+        subtree(): Builder;
+        children(): Builder;
+        ofType(t: StateObject.Type): Builder;
+        ancestorOfType(t: StateObject.Type): Builder;
+    }
+
+    const BuilderPrototype: any = {};
+
+    function registerModifier(name: string, f: Function) {
+        BuilderPrototype[name] = function (this: any, ...args: any[]) { return f.call(void 0, this, ...args) };
+    }
+
+    function build(compile: () => Query): Builder {
+        return Object.create(BuilderPrototype, { compile: { writable: false, configurable: false, value: compile } });
+    }
+
+    export function root() { return build(() => (state: State) => [state.objects.get(state.tree.rootRef)!]) }
+
+
+    export function byRef(...refs: string[]) {
+        return build(() => (state: State) => {
+            const ret: StateObject.Node[] = [];
+            for (const ref of refs) {
+                const n = state.objects.get(ref);
+                if (!n) continue;
+                ret.push(n);
+            }
+            return ret;
+        });
+    }
+
+    export function byValue(...objects: StateObject.Node[]) { return build(() => (state: State) => objects); }
+
+    registerModifier('flatMap', flatMap);
+    export function flatMap(b: Selector, f: (obj: StateObject.Node, state: State) => NodeSeq) {
+        const q = compile(b);
+        return build(() => (state: State) => {
+            const ret: StateObject.Node[] = [];
+            for (const n of q(state)) {
+                for (const m of f(n, state)) {
+                    ret.push(m);
+                }
+            }
+            return ret;
+        });
+    }
+
+    registerModifier('mapEntity', mapEntity);
+    export function mapEntity(b: Selector, f: (n: StateObject.Node, state: State) => StateObject.Node | undefined) {
+        const q = compile(b);
+        return build(() => (state: State) => {
+            const ret: StateObject.Node[] = [];
+            for (const n of q(state)) {
+                const x = f(n, state);
+                if (x) ret.push(x);
+            }
+            return ret;
+        });
+    }
+
+    registerModifier('unique', unique);
+    export function unique(b: Selector) {
+        const q = compile(b);
+        return build(() => (state: State) => {
+            const set = new Set<string>();
+            const ret: StateObject.Node[] = [];
+            for (const n of q(state)) {
+                if (!set.has(n.ref)) {
+                    set.add(n.ref);
+                    ret.push(n);
+                }
+            }
+            return ret;
+        })
+    }
+
+    registerModifier('first', first);
+    export function first(b: Selector) {
+        const q = compile(b);
+        return build(() => (state: State) => {
+            const r = q(state);
+            return r.length ? [r[0]] : [];
+        });
+    }
+
+    registerModifier('filter', filter);
+    export function filter(b: Selector, p: (n: StateObject.Node) => boolean) { return flatMap(b, n => p(n) ? [n] : []); }
+
+    registerModifier('subtree', subtree);
+    export function subtree(b: Selector) {
+        return flatMap(b, (n, s) => {
+            const nodes = [] as string[];
+            ImmutableTree.doPreOrder(s.tree, s.tree.nodes.get(n.ref), nodes, (x, _, ctx) => { ctx.push(x.ref) });
+            return nodes.map(x => s.objects.get(x)!);
+        });
+    }
+
+    registerModifier('children', children);
+    export function children(b: Selector) {
+        return flatMap(b, (n, s) => {
+            const nodes: StateObject.Node[] = [];
+            s.tree.nodes.get(n.ref)!.children.forEach(c => nodes.push(s.objects.get(c!)!));
+            return nodes;
+        });
+    }
+
+    registerModifier('ofType', ofType);
+    export function ofType(b: Selector, t: StateObject.Type) { return filter(b, n => n.obj ? n.obj.type === t : false); }
+
+    registerModifier('ancestorOfType', ancestorOfType);
+    export function ancestorOfType(b: Selector, t: StateObject.Type) { return unique(mapEntity(b, (n, s) => findAncestorOfType(s, n.ref, t))); }
+
+    registerModifier('parent', parent);
+    export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.objects.get(s.tree.nodes.get(n.ref)!.parent))); }
+
+    function findAncestorOfType({ tree, objects }: State, root: string, type: StateObject.Type): StateObject.Node | undefined {
+        let current = tree.nodes.get(root)!;
+        while (true) {
+            current = tree.nodes.get(current.parent)!;
+            if (current.ref === tree.rootRef) {
+                return objects.get(tree.rootRef);
+            }
+            const obj = objects.get(current.ref)!.obj!;
+            if (obj.type === type) return objects.get(current.ref);
+        }
+    }
+}
+
+export { StateSelection }

+ 20 - 14
src/mol-state/state.ts

@@ -6,7 +6,7 @@
 
 import { StateObject } from './object';
 import { StateTree } from './tree';
-import { Transform } from './tree/transform';
+import { Transform } from './transform';
 import { ImmutableTree } from './util/immutable-tree';
 import { Transformer } from './transformer';
 import { StateContext } from './context';
@@ -23,13 +23,19 @@ export namespace State {
     export type Ref = Transform.Ref
     export type Objects = Map<Ref, StateObject.Node>
 
-    export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) {
+    export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps: unknown }) {
         const tree = StateTree.create();
         const objects: Objects = new Map();
         const root = tree.getValue(tree.rootRef)!;
         const defaultObjectProps = (params && params.defaultObjectProps) || { }
 
-        objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version, props: { ...defaultObjectProps } });
+        objects.set(tree.rootRef, {
+            ref: tree.rootRef,
+            obj: rootObject,
+            state: StateObject.StateType.Ok,
+            version: root.version,
+            props: { ...defaultObjectProps }
+        });
 
         return {
             tree,
@@ -126,7 +132,7 @@ export namespace State {
             obj.state = state;
             obj.errorText = errorText;
         } else {
-            const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
+            const obj: StateObject.Node = { ref, state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
             ctx.objects.set(ref, obj);
             changed = true;
         }
@@ -159,7 +165,7 @@ export namespace State {
         }
     }
 
-    function findParent(tree: StateTree, objects: Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject {
+    function findAncestor(tree: StateTree, objects: Objects, root: Ref, types: { type: StateObject.Type }[]): StateObject {
         let current = tree.nodes.get(root)!;
         while (true) {
             current = tree.nodes.get(current.parent)!;
@@ -178,11 +184,11 @@ export namespace State {
             const update = await updateNode(ctx, root);
             setObjectState(ctx, root, StateObject.StateType.Ok);
             if (update.action === 'created') {
-                ctx.stateCtx.events.object.created.next({ ref: root });
+                ctx.stateCtx.events.object.created.next({ ref: root, obj: update.obj! });
             } else if (update.action === 'updated') {
-                ctx.stateCtx.events.object.updated.next({ ref: root });
+                ctx.stateCtx.events.object.updated.next({ ref: root, obj: update.obj });
             } else if (update.action === 'replaced') {
-                ctx.stateCtx.events.object.replaced.next({ ref: root, old: update.old });
+                ctx.stateCtx.events.object.replaced.next({ ref: root, oldObj: update.oldObj, newObj: update.newObj });
             }
         } catch (e) {
             doError(ctx, root, '' + e);
@@ -200,19 +206,19 @@ export namespace State {
     async function updateNode(ctx: UpdateContext, currentRef: Ref) {
         const { oldTree, tree, objects } = ctx;
         const transform = tree.getValue(currentRef)!;
-        const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from);
+        const parent = findAncestor(tree, objects, currentRef, transform.transformer.definition.from);
         // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
         if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) {
             // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
             const obj = await createObject(ctx, transform.transformer, parent, transform.params);
-            obj.ref = currentRef;
             objects.set(currentRef, {
+                ref: currentRef,
                 obj,
                 state: StateObject.StateType.Ok,
                 version: transform.version,
                 props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps }
             });
-            return { action: 'created' };
+            return { action: 'created', obj };
         } else {
             // console.log('updating...', transform.transformer.id);
             const current = objects.get(currentRef)!;
@@ -220,19 +226,19 @@ export namespace State {
             switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
                 case Transformer.UpdateResult.Recreate: {
                     const obj = await createObject(ctx, transform.transformer, parent, transform.params);
-                    obj.ref = currentRef;
                     objects.set(currentRef, {
+                        ref: currentRef,
                         obj,
                         state: StateObject.StateType.Ok,
                         version: transform.version,
                         props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }
                     });
-                    return { action: 'replaced', old: current.obj! };
+                    return { action: 'replaced', oldObj: current.obj!, newObj: obj };
                 }
                 case Transformer.UpdateResult.Updated:
                     current.version = transform.version;
                     current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps };
-                    return { action: 'updated' };
+                    return { action: 'updated', obj: current.obj };
                 default:
                     // TODO check if props need to be updated
                     return { action: 'none' };

+ 8 - 14
src/mol-state/tree/transform.ts → src/mol-state/transform.ts

@@ -4,8 +4,8 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateObject } from '../object';
-import { Transformer } from '../transformer';
+import { StateObject } from './object';
+import { Transformer } from './transformer';
 import { UUID } from 'mol-util';
 
 export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
@@ -19,22 +19,16 @@ export interface Transform<A extends StateObject = StateObject, B extends StateO
 export namespace Transform {
     export type Ref = string
 
-    export interface Props {
-        ref: Ref
-    }
-
-    export enum Flags {
-        // Indicates that the transform was generated by a behaviour and should not be automatically updated
-        Generated
-    }
+    export interface Options { ref?: Ref, defaultProps?: unknown }
 
-    export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, props?: Partial<Props>): Transform<A, B, P> {
-        const ref = props && props.ref ? props.ref : UUID.create() as string as Ref;
+    export function create<A extends StateObject, B extends StateObject, P>(transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
+        const ref = options && options.ref ? options.ref : UUID.create() as string as Ref;
         return {
             transformer,
-            params: params || { } as any,
+            params: params || {} as any,
             ref,
-            version: UUID.create()
+            version: UUID.create(),
+            defaultProps: options && options.defaultProps
         }
     }
 

+ 2 - 2
src/mol-state/transformer.ts

@@ -6,10 +6,10 @@
 
 import { Task } from 'mol-task';
 import { StateObject } from './object';
-import { Transform } from './tree/transform';
+import { Transform } from './transform';
 
 export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
-    apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>,
+    apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>,
     readonly namespace: string,
     readonly id: Transformer.Id,
     readonly definition: Transformer.Definition<A, B, P>

+ 2 - 2
src/mol-state/tree.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Transform } from './tree/transform';
+import { Transform } from './transform';
 import { ImmutableTree } from './util/immutable-tree';
 import { Transformer } from './transformer';
 import { StateObject } from './object';
@@ -59,7 +59,7 @@ namespace StateTree {
         }
 
         export class To<A extends StateObject> implements Builder {
-            apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Props>): To<Transformer.To<T>> {
+            apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, props?: Partial<Transform.Options>): To<Transformer.To<T>> {
                 const t = tr.apply(params, props);
                 this.state.tree.add(this.ref, t);
                 return new To(this.state, t.ref);

+ 12 - 6
src/perf-tests/state.ts

@@ -1,4 +1,4 @@
-import { State, StateObject, StateTree, Transformer } from 'mol-state';
+import { State, StateObject, StateTree, Transformer, StateSelection } from 'mol-state';
 import { Task } from 'mol-task';
 import * as util from 'util';
 
@@ -9,10 +9,10 @@ export interface TypeInfo { name: string, class: TypeClass }
 const _obj = StateObject.factory<TypeInfo, ObjProps>()
 const _transform = Transformer.factory('test');
 
-export class Root extends _obj('root', { name: 'Root', class: 'root' }) { }
-export class Square extends _obj<{ a: number }>('square', { name: 'Square', class: 'shape' }) { }
-export class Circle extends _obj<{ r: number }>('circle', { name: 'Circle', class: 'shape' }) { }
-export class Area extends _obj<{ volume: number }>('volume', { name: 'Volume', class: 'prop' }) { }
+export class Root extends _obj({ name: 'Root', class: 'root' }) { }
+export class Square extends _obj<{ a: number }>({ name: 'Square', class: 'shape' }) { }
+export class Circle extends _obj<{ r: number }>({ name: 'Circle', class: 'shape' }) { }
+export class Area extends _obj<{ volume: number }>({ name: 'Area', class: 'prop' }) { }
 
 export const CreateSquare = _transform<Root, Square, { a: number }>({
     name: 'create-square',
@@ -74,7 +74,7 @@ function hookEvents(state: State) {
 }
 
 export async function testState() {
-    const state = State.create();
+    const state = State.create(new Root({ label: 'Root' }, { }));
     hookEvents(state);
 
     const tree = state.tree;
@@ -105,6 +105,12 @@ export async function testState() {
     console.log('----------------');
     const state2 = await State.update(state1, treeFromJson).run();
     console.log(util.inspect(state2.objects, true, 3, true));
+
+    console.log('----------------');
+
+    const q = StateSelection.byRef('square').parent();
+    const sel = StateSelection.select(q, state2);
+    console.log(sel);
 }
 
 testState();