Bladeren bron

mol-state & plugin: wip

David Sehnal 6 jaren geleden
bovenliggende
commit
e463e02b88

+ 6 - 2
src/mol-plugin/behavior.ts

@@ -5,10 +5,14 @@
  */
 
 export * from './behavior/behavior'
-import * as Data from './behavior/built-in/state'
+
+import * as State from './behavior/built-in/state'
 import * as Representation from './behavior/built-in/representation'
 
+export const BuiltInPluginBehaviors = {
+    State,
+}
+
 export const PluginBehaviors = {
-    Data,
     Representation
 }

+ 44 - 19
src/mol-plugin/behavior/built-in/state.ts

@@ -4,26 +4,51 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { PluginBehavior } from '../behavior';
 import { PluginCommands } from '../../command';
+import { PluginContext } from '../../context';
 
-export const SetCurrentObject = PluginBehavior.create({
-    name: 'set-current-data-object-behavior',
-    ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.SetCurrentObject, ({ state, ref }, ctx) => state.setCurrent(ref)),
-    display: { name: 'Set Current Handler', group: 'Data' }
-});
-
-export const Update = PluginBehavior.create({
-    name: 'update-data-behavior',
-    ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))),
-    display: { name: 'Update Data Handler', group: 'Data' }
-});
-
-export const RemoveObject = PluginBehavior.create({
-    name: 'remove-object-data-behavior',
-    ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.RemoveObject, ({ state, ref }, ctx) => {
+export function SetCurrentObject(ctx: PluginContext) {
+    PluginCommands.State.SetCurrentObject.subscribe(ctx, ({ state, ref }) => state.setCurrent(ref));
+}
+
+export function Update(ctx: PluginContext) {
+    PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.update(tree)));
+}
+
+export function ApplyAction(ctx: PluginContext) {
+    PluginCommands.State.ApplyAction.subscribe(ctx, ({ state, action, ref }) => ctx.runTask(state.apply(action.action, action.params, ref)));
+}
+
+export function RemoveObject(ctx: PluginContext) {
+    PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => {
         const tree = state.tree.build().delete(ref).getTree();
         return ctx.runTask(state.update(tree));
-    }),
-    display: { name: 'Remove Object Handler', group: 'Data' }
-});
+    });
+}
+
+// export const SetCurrentObject = PluginBehavior.create({
+//     name: 'set-current-data-object-behavior',
+//     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.SetCurrentObject, ({ state, ref }, ctx) => state.setCurrent(ref)),
+//     display: { name: 'Set Current Handler', group: 'Data' }
+// });
+
+// export const Update = PluginBehavior.create({
+//     name: 'update-data-behavior',
+//     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))),
+//     display: { name: 'Update Data Handler', group: 'Data' }
+// });
+
+// export const ApplyAction = PluginBehavior.create({
+//     name: 'update-data-behavior',
+//     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.Update, ({ state, tree }, ctx) => ctx.runTask(state.update(tree))),
+//     display: { name: 'Update Data Handler', group: 'Data' }
+// });
+
+// export const RemoveObject = PluginBehavior.create({
+//     name: 'remove-object-data-behavior',
+//     ctor: PluginBehavior.simpleCommandHandler(PluginCommands.State.RemoveObject, ({ state, ref }, ctx) => {
+//         const tree = state.tree.build().delete(ref).getTree();
+//         return ctx.runTask(state.update(tree));
+//     }),
+//     display: { name: 'Remove Object Handler', group: 'Data' }
+// });

+ 7 - 2
src/mol-plugin/command/command.ts

@@ -111,14 +111,18 @@ namespace PluginCommand {
             }
         }
 
+        private executing = false;
         private async next() {
-            if (this.queue.count === 0) return;
+            if (this.queue.count === 0 || this.executing) return;
             const cmd = this.queue.removeFirst()!;
 
             const actions = this.subs.get(cmd.id);
-            if (!actions) return;
+            if (!actions) {
+                return;
+            }
 
             try {
+                this.executing = true;
                 // TODO: should actions be called "asynchronously" ("setImmediate") instead?
                 for (const a of actions) {
                     await a(cmd.params);
@@ -127,6 +131,7 @@ namespace PluginCommand {
             } catch (e) {
                 cmd.reject(e);
             } finally {
+                this.executing = false;
                 if (!this.disposing) this.next();
             }
         }

+ 2 - 0
src/mol-plugin/command/state.ts

@@ -6,8 +6,10 @@
 
 import { PluginCommand } from './command';
 import { Transform, State } from 'mol-state';
+import { StateAction } from 'mol-state/action';
 
 export const SetCurrentObject = PluginCommand<{ state: State, ref: Transform.Ref }>('ms-data', 'set-current-object');
+export const ApplyAction = PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>('ms-data', 'apply-action');
 export const Update = PluginCommand<{ state: State, tree: State.Tree | State.Builder }>('ms-data', 'update');
 
 // export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object');

+ 23 - 55
src/mol-plugin/context.ts

@@ -10,13 +10,13 @@ import { StateTransforms } from './state/transforms';
 import { PluginStateObject as SO } from './state/objects';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { PluginState } from './state';
-import { MolScriptBuilder } from 'mol-script/language/builder';
 import { PluginCommand, PluginCommands } from './command';
 import { Task } from 'mol-task';
 import { merge } from 'rxjs';
-import { PluginBehaviors } from './behavior';
+import { PluginBehaviors, BuiltInPluginBehaviors } from './behavior';
 import { Loci, EmptyLoci } from 'mol-model/loci';
 import { Representation } from 'mol-repr';
+import { CreateStructureFromPDBe } from './state/actions/basic';
 
 export class PluginContext {
     private disposed = false;
@@ -69,7 +69,7 @@ export class PluginContext {
     }
 
     async runTask<T>(task: Task<T>) {
-        return await task.run(p => console.log(p), 250);
+        return await task.run(p => console.log(p.root.progress.message), 250);
     }
 
     dispose() {
@@ -81,12 +81,16 @@ export class PluginContext {
         this.disposed = true;
     }
 
-    async _test_initBehaviours() {
+    private initBuiltInBehavior() {
+        BuiltInPluginBehaviors.State.ApplyAction(this);
+        BuiltInPluginBehaviors.State.RemoveObject(this);
+        BuiltInPluginBehaviors.State.SetCurrentObject(this);
+        BuiltInPluginBehaviors.State.Update(this);
+    }
+
+    async _test_initBehaviors() {
         const tree = this.state.behavior.tree.build()
-            .toRoot().apply(PluginBehaviors.Data.SetCurrentObject, { ref: PluginBehaviors.Data.SetCurrentObject.id })
-            .and().toRoot().apply(PluginBehaviors.Data.Update, { ref: PluginBehaviors.Data.Update.id })
-            .and().toRoot().apply(PluginBehaviors.Data.RemoveObject, { ref: PluginBehaviors.Data.RemoveObject.id })
-            .and().toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id })
+            .toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas, { ref: PluginBehaviors.Representation.AddRepresentationToCanvas.id })
             .and().toRoot().apply(PluginBehaviors.Representation.HighlightLoci, { ref: PluginBehaviors.Representation.HighlightLoci.id })
             .and().toRoot().apply(PluginBehaviors.Representation.SelectLoci, { ref: PluginBehaviors.Representation.SelectLoci.id })
             .getTree();
@@ -94,6 +98,12 @@ export class PluginContext {
         await this.runTask(this.state.behavior.update(tree));
     }
 
+    _test_initDataActions() {
+        this.state.data.actions
+            .add(CreateStructureFromPDBe)
+            .add(StateTransforms.Data.Download.toAction());
+    }
+
     applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) {
         const tree = state.tree.build().to(a).apply(transformer, params);
         return PluginCommands.State.Update.dispatch(this, { state, tree });
@@ -104,32 +114,8 @@ export class PluginContext {
         return PluginCommands.State.Update.dispatch(this, { state, tree });
     }
 
-    _test_createState(url: string) {
-        const b = this.state.data.tree.build();
-
-        const query = MolScriptBuilder.struct.generator.atomGroups({
-            // 'atom-test': MolScriptBuilder.core.rel.eq([
-            //     MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
-            //     MolScriptBuilder.es('C')
-            // ]),
-            'residue-test': MolScriptBuilder.core.rel.eq([
-                MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
-                'ALA'
-            ])
-        });
-
-        const newTree = b.toRoot()
-            .apply(StateTransforms.Data.Download, { url })
-            .apply(StateTransforms.Data.ParseCif)
-            .apply(StateTransforms.Model.ParseTrajectoryFromMmCif, {}, { ref: 'trajectory' })
-            .apply(StateTransforms.Model.CreateModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' })
-            .apply(StateTransforms.Model.CreateStructureFromModel, { }, { ref: 'structure' })
-            .apply(StateTransforms.Model.CreateStructureAssembly)
-            .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' })
-            .apply(StateTransforms.Visuals.CreateStructureRepresentation)
-            .getTree();
-
-        this.runTask(this.state.data.update(newTree));
+    _test_createState(id: string) {
+        this.runTask(this.state.data.apply(CreateStructureFromPDBe, { id }));
     }
 
     private initEvents() {
@@ -159,30 +145,12 @@ export class PluginContext {
         this.canvas3d.requestDraw(true);
     }
 
-    async _test_nextModel() {
-        const traj = this.state.data.select('trajectory')[0].obj as SO.Molecule.Trajectory;
-        //const modelIndex = (this.state.data.select('model')[0].transform.params as CreateModelFromTrajectory.Params).modelIndex;
-        const newTree = this.state.data.build().to('model').update(
-            StateTransforms.Model.CreateModelFromTrajectory,
-            old => ({ modelIndex: (old.modelIndex + 1) % traj.data.length }))
-            .getTree();
-        // const newTree = StateTree.updateParams(this.state.data.tree, 'model', { modelIndex: (modelIndex + 1) % traj.data.length });
-        await this.runTask(this.state.data.update(newTree));
-        // this.viewer.requestDraw(true);
-    }
-
-    _test_playModels() {
-        const update = async () => {
-            await this._test_nextModel();
-            setTimeout(update, 1000 / 15);
-        }
-        update();
-    }
-
     constructor() {
         this.initEvents();
+        this.initBuiltInBehavior();
 
-        this._test_initBehaviours();
+        this._test_initBehaviors();
+        this._test_initDataActions();
     }
 
     // logger = ;

+ 84 - 1
src/mol-plugin/state/actions/basic.ts

@@ -4,4 +4,87 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-// TODO: basic actions like "download and create default representation"
+import { StateAction } from 'mol-state/action';
+import { PluginStateObject } from '../objects';
+import { StateTransforms } from '../transforms';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { StateSelection } from 'mol-state/state/selection';
+
+export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root, void, { id: string }>({
+    from: [PluginStateObject.Root],
+    display: {
+        name: 'Entry from PDBe',
+        description: 'Download a structure from PDBe and create its default Assembly and visual'
+    },
+    params: {
+        default: () => ({ id: '1grm' }),
+        controls: () => ({
+            id: PD.Text('PDB id', '', '1grm'),
+        })
+    },
+    apply({ params, state }) {
+        const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`;
+        const b = state.build();
+
+        // const query = MolScriptBuilder.struct.generator.atomGroups({
+        //     // 'atom-test': MolScriptBuilder.core.rel.eq([
+        //     //     MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
+        //     //     MolScriptBuilder.es('C')
+        //     // ]),
+        //     'residue-test': MolScriptBuilder.core.rel.eq([
+        //         MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(),
+        //         'ALA'
+        //     ])
+        // });
+
+        const newTree = b.toRoot()
+            .apply(StateTransforms.Data.Download, { url })
+            .apply(StateTransforms.Data.ParseCif)
+            .apply(StateTransforms.Model.ParseTrajectoryFromMmCif, {})
+            .apply(StateTransforms.Model.CreateModelFromTrajectory, { modelIndex: 0 })
+            .apply(StateTransforms.Model.CreateStructureFromModel, { })
+            .apply(StateTransforms.Model.CreateStructureAssembly)
+            // .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' })
+            .apply(StateTransforms.Visuals.CreateStructureRepresentation)
+            .getTree();
+
+        return state.update(newTree);
+    }
+});
+
+export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void, { action: 'advance' | 'reset', by?: number }>({
+    from: [],
+    display: {
+        name: 'Entry from PDBe',
+        description: 'Download a structure from PDBe and create its default Assembly and visual'
+    },
+    params: {
+        default: () => ({ action: 'reset', by: 1 })
+    },
+    apply({ params, state }) {
+        const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model).filter(c => c.transform.transformer === StateTransforms.Model.CreateModelFromTrajectory));
+
+        const update = state.build();
+
+        if (params.action === 'reset') {
+            for (const m of models) {
+                update.to(m.transform.ref).update(StateTransforms.Model.CreateModelFromTrajectory,
+                    () => ({ modelIndex: 0}));
+            }
+        } else {
+            for (const m of models) {
+                const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+                if (!parent || !parent.obj) continue;
+                const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
+                update.to(m.transform.ref).update(StateTransforms.Model.CreateModelFromTrajectory,
+                    old => {
+                        let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                        if (modelIndex < 0) modelIndex += traj.data.length;
+                        return { modelIndex };
+                    });
+            }
+        }
+
+        return state.update(update);
+    }
+});

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

@@ -62,7 +62,6 @@ const CreateModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajec
     },
     isApplicable: a => a.data.length > 0,
     apply({ a, params }) {
-        console.log('parans', params);
         if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`);
         const model = a.data[params.modelIndex];
         const label = { label: `Model ${model.modelNum}` };

+ 46 - 27
src/mol-plugin/ui/controls.tsx

@@ -6,18 +6,15 @@
 
 import * as React from 'react';
 import { PluginContext } from '../context';
-import { Transform, Transformer, State } from 'mol-state';
+import { Transform, State } from 'mol-state';
 import { ParametersComponent } from 'mol-app/component/parameters';
+import { StateAction } from 'mol-state/action';
+import { PluginCommands } from 'mol-plugin/command';
+import { UpdateTrajectory } from 'mol-plugin/state/actions/basic';
 
 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);
-    }
-
     private _snap: any = void 0;
     private getSnapshot = () => {
         this._snap = this.props.plugin.state.getSnapshot();
@@ -30,11 +27,7 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s
 
     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/>
+            <button onClick={() => this.props.plugin._test_centerView()}>Center View</button><br />
             <hr />
             <button onClick={this.getSnapshot}>Get Snapshot</button>
             <button onClick={this.setSnapshot}>Set Snapshot</button>
@@ -42,31 +35,57 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s
     }
 }
 
-export class _test_CreateTransform extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, state: State, transformer: Transformer }, { params: any }> {
+
+export class _test_TrajectoryControls extends React.Component<{ plugin: PluginContext }> {
+    render() {
+        return <div>
+            <b>Trajectory: </b>
+            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, {
+                state: this.props.plugin.state.data,
+                action: UpdateTrajectory.create({ action: 'advance', by: -1 })
+            })}>&lt;&lt;</button>
+            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, {
+                state: this.props.plugin.state.data,
+                action: UpdateTrajectory.create({ action: 'reset' })
+            })}>Reset</button>
+            <button onClick={() => PluginCommands.State.ApplyAction.dispatch(this.props.plugin, {
+                state: this.props.plugin.state.data,
+                action: UpdateTrajectory.create({ action: 'advance', by: +1 })
+            })}>&gt;&gt;</button><br />
+        </div>
+    }
+}
+
+export class _test_ApplyAction extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, state: State, action: StateAction }, { params: any }> {
     private getObj() {
         const obj = this.props.state.cells.get(this.props.nodeRef)!;
         return obj;
     }
 
     private getDefaultParams() {
-        const p = this.props.transformer.definition.params;
-        if (!p || !p.default) return { };
+        const p = this.props.action.definition.params;
+        if (!p || !p.default) return {};
         const obj = this.getObj();
-        if (!obj.obj) return { };
+        if (!obj.obj) return {};
         return p.default(obj.obj, this.props.plugin);
     }
 
     private getParamDef() {
-        const p = this.props.transformer.definition.params;
-        if (!p || !p.controls) return { };
+        const p = this.props.action.definition.params;
+        if (!p || !p.controls) return {};
         const obj = this.getObj();
-        if (!obj.obj) return { };
+        if (!obj.obj) return {};
         return p.controls(obj.obj, this.props.plugin);
     }
 
     private create() {
-        console.log(this.props.transformer.definition.name, this.state.params);
-        this.props.plugin.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params);
+        console.log('Apply Action', this.state.params);
+        PluginCommands.State.ApplyAction.dispatch(this.props.plugin, {
+            state: this.props.state,
+            action: this.props.action.create(this.state.params),
+            ref: this.props.nodeRef
+        });
+        // this.props.plugin.applyTransform(this.props.state, this.props.nodeRef, this.props.transformer, this.state.params);
     }
 
     state = { params: this.getDefaultParams() }
@@ -78,10 +97,10 @@ export class _test_CreateTransform extends React.Component<{ plugin: PluginConte
             return <div />;
         }
 
-        const t = this.props.transformer;
+        const action = this.props.action;
 
-        return <div key={`${this.props.nodeRef} ${this.props.transformer.id}`}>
-            <div style={{ borderBottom: '1px solid #999'}}>{(t.definition.display && t.definition.display.name) || t.definition.name}</div>
+        return <div key={`${this.props.nodeRef} ${this.props.action.id}`}>
+            <div style={{ borderBottom: '1px solid #999' }}><h3>{(action.definition.display && action.definition.display.name) || action.id}</h3></div>
             <ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={(k, v) => {
                 this.setState({ params: { ...this.state.params, [k]: v } });
             }} />
@@ -97,7 +116,7 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte
 
     private getDefParams() {
         const cell = this.getCell();
-        if (!cell) return { };
+        if (!cell) return {};
         return cell.transform.params;
     }
 
@@ -136,8 +155,8 @@ export class _test_UpdateTransform extends React.Component<{ plugin: PluginConte
 
         const tr = transform.transformer;
 
-        return <div key={`${this.props.nodeRef} ${tr.id}`}>
-            <div style={{ borderBottom: '1px solid #999'}}>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</div>
+        return <div key={`${this.props.nodeRef} ${tr.id}`} style={{ marginBottom: '10ox' }}>
+            <div style={{ borderBottom: '1px solid #999' }}><h3>{(tr.definition.display && tr.definition.display.name) || tr.definition.name}</h3></div>
             <ParametersComponent params={params} values={this.state.params as any} onChange={(k, v) => {
                 this.setState({ params: { ...this.state.params, [k]: v } });
             }} />

+ 15 - 13
src/mol-plugin/ui/plugin.tsx

@@ -8,26 +8,27 @@ import * as React from 'react';
 import { PluginContext } from '../context';
 import { StateTree } from './state-tree';
 import { Viewport } from './viewport';
-import { Controls, _test_CreateTransform, _test_UpdateTransform } from './controls';
-import { Transformer } from 'mol-state';
+import { Controls, _test_UpdateTransform, _test_ApplyAction, _test_TrajectoryControls } from './controls';
 
 // TODO: base object with subscribe helpers, separate behavior list etc
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, { }> {
     render() {
         return <div style={{ position: 'absolute', width: '100%', height: '100%', fontFamily: 'monospace' }}>
-            <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll' }}>
-                <h3>Data</h3>
+            <div style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll', padding: '10px' }}>
                 <StateTree plugin={this.props.plugin} state={this.props.plugin.state.data} />
-                <hr />
-                <_test_CurrentObject plugin={this.props.plugin} />
                 <h3>Behaviors</h3>
                 <StateTree plugin={this.props.plugin} state={this.props.plugin.state.behavior} />
             </div>
-            <div style={{ position: 'absolute', left: '350px', right: '250px', height: '100%' }}>
+            <div style={{ position: 'absolute', left: '350px', right: '300px', height: '100%' }}>
                 <Viewport plugin={this.props.plugin} />
+                <div style={{ position: 'absolute', left: '10px', top: '10px', height: '100%', color: 'white' }}>
+                    <_test_TrajectoryControls {...this.props} />
+                </div>
             </div>
-            <div style={{ position: 'absolute', width: '250px', right: '0', height: '100%' }}>
+            <div style={{ position: 'absolute', width: '300px', right: '0', height: '100%', padding: '10px' }}>
+                <_test_CurrentObject plugin={this.props.plugin} />
+                <hr />
                 <Controls plugin={this.props.plugin} />
             </div>
         </div>;
@@ -41,24 +42,25 @@ export class _test_CurrentObject extends React.Component<{ plugin: PluginContext
     }
     render() {
         const current = this.props.plugin.behaviors.state.data.currentObject.value;
+
         const ref = current.ref;
         // const n = this.props.plugin.state.data.tree.nodes.get(ref)!;
         const obj = this.props.plugin.state.data.cells.get(ref)!;
 
         const type = obj && obj.obj ? obj.obj.type : void 0;
 
-        const transforms = type
-            ? Transformer.fromType(type)
+        const actions = type
+            ? current.state.actions.fromType(type)
             : []
         return <div>
-            Current Ref: {ref}
             <hr />
-            <h3>Update</h3>
+            <h3>Update {ref}</h3>
             <_test_UpdateTransform key={`${ref} update`} plugin={this.props.plugin} state={current.state} nodeRef={ref} />
             <hr />
             <h3>Create</h3>
             {
-                transforms.map((t, i) => <_test_CreateTransform key={`${t.id} ${ref} ${i}`} plugin={this.props.plugin} state={current.state} transformer={t} nodeRef={ref} />)
+                actions.map((act, i) => <_test_ApplyAction key={`${act.id} ${ref} ${i}`}
+                    plugin={this.props.plugin} state={current.state} action={act} nodeRef={ref} />)
             }
         </div>;
     }

+ 12 - 1
src/mol-state/action.ts

@@ -14,6 +14,7 @@ import { Transformer } from './transformer';
 export { StateAction };
 
 interface StateAction<A extends StateObject = StateObject, T = any, P = unknown> {
+    create(params: P): StateAction.Instance,
     readonly id: UUID,
     readonly definition: StateAction.Definition<A, T, P>
 }
@@ -24,6 +25,11 @@ namespace StateAction {
     export type ReType<T extends StateAction<any, any, any>> = T extends StateAction<any, infer T, any> ? T : unknown;
     export type ControlsFor<Props> = { [P in keyof Props]?: PD.Any }
 
+    export interface Instance {
+        action: StateAction,
+        params: any
+    }
+
     export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
         cell: StateObjectCell,
         a: A,
@@ -47,7 +53,12 @@ namespace StateAction {
     }
 
     export function create<A extends StateObject, T, P>(definition: Definition<A, T, P>): StateAction<A, T, P> {
-        return { id: UUID.create(), definition };
+        const action: StateAction<A, T, P> = {
+            create(params) { return { action, params }; },
+            id: UUID.create(),
+            definition
+        };
+        return action;
     }
 
     export function fromTransformer<T extends Transformer>(transformer: T) {

+ 35 - 0
src/mol-state/action/manager.ts

@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateAction } from 'mol-state/action';
+import { StateObject } from '../object';
+
+export { StateActionManager }
+
+class StateActionManager {
+    private actions: Map<StateAction['id'], StateAction> = new Map();
+    private fromTypeIndex = new Map<StateObject.Type, StateAction[]>();
+
+    add(action: StateAction) {
+        if (this.actions.has(action.id)) return this;
+
+        this.actions.set(action.id, action);
+
+        for (const t of action.definition.from) {
+            if (this.fromTypeIndex.has(t.type)) {
+                this.fromTypeIndex.get(t.type)!.push(action);
+            } else {
+                this.fromTypeIndex.set(t.type, [action]);
+            }
+        }
+
+        return this;
+    }
+
+    fromType(type: StateObject.Type): ReadonlyArray<StateAction> {
+        return this.fromTypeIndex.get(type) || [];
+    }
+}

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

@@ -14,6 +14,7 @@ import { StateSelection } from './state/selection';
 import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { StateTreeBuilder } from './tree/builder';
 import { StateAction } from './action';
+import { StateActionManager } from './action/manager';
 
 export { State }
 
@@ -42,6 +43,8 @@ class State {
         currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef })
     };
 
+    readonly actions = new StateActionManager();
+
     get tree() { return this._tree; }
     get current() { return this._current; }
 
@@ -81,8 +84,15 @@ class State {
         return StateSelection.select(selector(StateSelection.Generators), this)
     }
 
-    apply(action: StateAction, ref: Transform.Ref) {
-        
+    /** Is no ref is specified, apply to root */
+    apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
+        return Task.create('Apply Action', ctx => {
+            const cell = this.cells.get(ref);
+            if (!cell) throw new Error(`'${ref}' does not exist.`);
+            if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`);
+
+            return runTask(action.definition.apply({ cell, a: cell.obj!, params, state: this }, this.globalContext), ctx);
+        });
     }
 
     update(tree: StateTree | StateTreeBuilder): Task<void> {

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

@@ -87,6 +87,23 @@ namespace StateSelection {
         }
 
         export function byValue(...objects: StateObjectCell[]) { return build(() => (state: State) => objects); }
+
+        export function rootsOfType(type: StateObject.Ctor) {
+            return build(() => state => {
+                const ctx = { roots: [] as StateObjectCell[], cells: state.cells, type: type.type };
+                StateTree.doPreOrder(state.tree, state.tree.root, ctx, _findRootsOfType);
+                return ctx.roots;
+            });
+        }
+
+        function _findRootsOfType(n: Transform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) {
+            const cell = s.cells.get(n.ref);
+            if (cell && cell.obj && cell.obj.type === s.type) {
+                s.roots.push(cell);
+                return false;
+            }
+            return true;
+        }
     }
 
     registerModifier('flatMap', flatMap);

+ 10 - 0
src/mol-state/tree/builder.ts

@@ -9,6 +9,7 @@ import { TransientTree } from './transient';
 import { StateObject } from '../object';
 import { Transform } from '../transform';
 import { Transformer } from '../transformer';
+import { shallowEqual } from 'mol-util';
 
 export { StateTreeBuilder }
 
@@ -54,6 +55,15 @@ namespace StateTreeBuilder {
             } else {
                 params = paramsOrTransformer;
             }
+
+            if (old.transformer.definition.params && old.transformer.definition.params.areEqual) {
+                if (old.transformer.definition.params.areEqual(old.params, params)) return this.root;
+            } else {
+                if (shallowEqual(old.params, params)) {
+                    return this.root;
+                }
+            }
+
             this.state.tree.set(Transform.updateParams(old, params));
             return this.root;
         }