Browse Source

wip plugin

David Sehnal 6 years ago
parent
commit
248c6c07fc

+ 3 - 1
src/mol-plugin/behavior.ts

@@ -6,7 +6,9 @@
 
 export * from './behavior/behavior'
 import * as Data from './behavior/data'
+import * as Representation from './behavior/representation'
 
 export const PluginBehaviors = {
-    Data
+    Data,
+    Representation
 }

+ 26 - 4
src/mol-plugin/behavior/behavior.ts

@@ -10,6 +10,7 @@ import { Transformer } from 'mol-state';
 import { Task } from 'mol-task';
 import { PluginContext } from 'mol-plugin/context';
 import { PluginCommand } from '../command';
+import { Observable } from 'rxjs';
 
 export { PluginBehavior }
 
@@ -29,14 +30,14 @@ namespace PluginBehavior {
         ctor: Ctor<P>,
         label?: (params: P) => { label: string, description?: string },
         display: { name: string, description?: string },
-        params?: Transformer.Definition<SO.Root, SO.Behavior, P>['params']
+        params?: Transformer.Definition<SO.BehaviorRoot, SO.Behavior, P>['params']
     }
 
     export function create<P>(params: CreateParams<P>) {
-        return PluginStateTransform.Create<SO.Root, SO.Behavior, P>({
+        return PluginStateTransform.Create<SO.BehaviorRoot, SO.Behavior, P>({
             name: params.name,
             display: params.display,
-            from: [SO.Root],
+            from: [SO.BehaviorRoot],
             to: [SO.Behavior],
             params: params.params,
             apply({ params: p }, ctx: PluginContext) {
@@ -53,7 +54,7 @@ namespace PluginBehavior {
         });
     }
 
-    export function commandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) {
+    export function simpleCommandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) {
         return class implements PluginBehavior<undefined> {
             private sub: PluginCommand.Subscription | undefined = void 0;
             register(): void {
@@ -66,4 +67,25 @@ namespace PluginBehavior {
             constructor(private ctx: PluginContext) { }
         }
     }
+
+    export abstract class Handler implements PluginBehavior<undefined> {
+        private subs: PluginCommand.Subscription[] = [];
+        protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
+            this.subs.push(cmd.subscribe(this.ctx, action));
+        }
+        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
+            this.subs.push(o.subscribe(action));
+        }
+        protected track<T>(sub: PluginCommand.Subscription) {
+            this.subs.push(sub);
+        }
+        abstract register(): void;
+        unregister() {
+            for (const s of this.subs) s.unsubscribe();
+            this.subs = [];
+        }
+        constructor(protected ctx: PluginContext) {
+
+        }
+    }
 }

+ 2 - 17
src/mol-plugin/behavior/data.ts

@@ -1,4 +1,3 @@
-
 /**
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
@@ -8,28 +7,14 @@
 import { PluginBehavior } from './behavior';
 import { PluginCommands } from 'mol-plugin/command';
 
-// export class SetCurrentObject implements PluginBehavior<undefined> {
-//     private sub: PluginCommand.Subscription | undefined = void 0;
-
-//     register(): void {
-//         this.sub = PluginCommands.Data.SetCurrentObject.subscribe(this.ctx, ({ ref }) => this.ctx.state.data.setCurrent(ref));
-//     }
-//     unregister(): void {
-//         if (this.sub) this.sub.unsubscribe();
-//         this.sub = void 0;
-//     }
-
-//     constructor(private ctx: PluginContext) { }
-// }
-
 export const SetCurrentObject = PluginBehavior.create({
     name: 'set-current-data-object-behavior',
-    ctor: PluginBehavior.commandHandler(PluginCommands.Data.SetCurrentObject, ({ ref }, ctx) => ctx.state.data.setCurrent(ref)),
+    ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.SetCurrentObject, ({ ref }, ctx) => ctx.state.data.setCurrent(ref)),
     display: { name: 'Set Current Handler' }
 });
 
 export const Update = PluginBehavior.create({
     name: 'update-data-behavior',
-    ctor: PluginBehavior.commandHandler(PluginCommands.Data.Update, ({ tree }, ctx) => ctx.runTask(ctx.state.data.update(tree))),
+    ctor: PluginBehavior.simpleCommandHandler(PluginCommands.Data.Update, ({ tree }, ctx) => ctx.runTask(ctx.state.data.update(tree))),
     display: { name: 'Update Data Handler' }
 });

+ 50 - 0
src/mol-plugin/behavior/representation.ts

@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginBehavior } from './behavior';
+import { PluginStateObjects as SO } from '../state/objects';
+
+class _AddRepresentationToCanvas extends PluginBehavior.Handler {
+    register(): void {
+        this.subscribeObservable(this.ctx.events.state.data.object.created, o => {
+            if (!SO.StructureRepresentation3D.is(o.obj)) return;
+            this.ctx.canvas3d.add(o.obj.data.repr);
+            this.ctx.canvas3d.requestDraw(true);
+        });
+        this.subscribeObservable(this.ctx.events.state.data.object.updated, o => {
+            const oo = o.obj;
+            if (!SO.StructureRepresentation3D.is(oo)) return;
+            this.ctx.canvas3d.add(oo.data.repr);
+            this.ctx.canvas3d.requestDraw(true);
+        });
+        this.subscribeObservable(this.ctx.events.state.data.object.removed, o => {
+            const oo = o.obj;
+            console.log('removed', o.ref, oo && oo.type);
+            if (!SO.StructureRepresentation3D.is(oo)) return;
+            this.ctx.canvas3d.remove(oo.data.repr);
+            console.log('removed from canvas', o.ref);
+            this.ctx.canvas3d.requestDraw(true);
+            oo.data.repr.destroy();
+        });
+        this.subscribeObservable(this.ctx.events.state.data.object.replaced, o => {
+            if (o.oldObj && SO.StructureRepresentation3D.is(o.oldObj)) {
+                this.ctx.canvas3d.remove(o.oldObj.data.repr);
+                this.ctx.canvas3d.requestDraw(true);
+                o.oldObj.data.repr.destroy();
+            }
+            if (o.newObj && SO.StructureRepresentation3D.is(o.newObj)) {
+                this.ctx.canvas3d.add(o.newObj.data.repr);
+                this.ctx.canvas3d.requestDraw(true);
+            }
+        });
+    }
+}
+
+export const AddRepresentationToCanvas = PluginBehavior.create({
+    name: 'add-representation-to-canvas',
+    ctor: _AddRepresentationToCanvas,
+    display: { name: 'Add Representation To Canvas' }
+});

+ 1 - 1
src/mol-plugin/command/command.ts

@@ -87,7 +87,7 @@ namespace PluginCommand {
         /** Resolves after all actions have completed */
         dispatch<T>(cmd: PluginCommand<T> | Id, params: T) {
             return new Promise<void>((resolve, reject) => {
-                if (!this.disposing) {
+                if (this.disposing) {
                     reject('disposed');
                     return;
                 }

+ 45 - 15
src/mol-plugin/context.ts

@@ -4,15 +4,17 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateTree, StateSelection, Transformer } from 'mol-state';
+import { StateTree, StateSelection, Transformer, Transform } from 'mol-state';
 import Canvas3D from 'mol-canvas3d/canvas3d';
 import { StateTransforms } from './state/transforms';
 import { PluginStateObjects 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 } from './command';
+import { PluginCommand, PluginCommands } from './command';
 import { Task } from 'mol-task';
+import { merge } from 'rxjs';
+import { PluginBehaviors } from './behavior';
 
 export class PluginContext {
     private disposed = false;
@@ -22,10 +24,17 @@ export class PluginContext {
     readonly commands = new PluginCommand.Manager();
 
     readonly events = {
-        data: this.state.data.context.events
+        state: {
+            data: this.state.data.context.events,
+            behavior: this.state.behavior.context.events
+        }
     };
 
     readonly behaviors = {
+        state: {
+            data: this.state.data.context.behaviors,
+            behavior: this.state.behavior.context.behaviors
+        },
         command: this.commands.behaviour
     };
 
@@ -66,6 +75,21 @@ export class PluginContext {
         this.disposed = true;
     }
 
+    async _test_initBehaviours() {
+        const tree = StateTree.build(this.state.behavior.tree)
+            .toRoot().apply(PluginBehaviors.Data.SetCurrentObject)
+            .and().toRoot().apply(PluginBehaviors.Data.Update)
+            .and().toRoot().apply(PluginBehaviors.Representation.AddRepresentationToCanvas)
+            .getTree();
+
+        await this.state.updateBehaviour(tree);
+    }
+
+    _test_applyTransform(a: Transform.Ref, transformer: Transformer, params: any) {
+        const tree = StateTree.build(this.state.data.tree).to(a).apply(transformer, params).getTree();
+        PluginCommands.Data.Update.dispatch(this, { tree });
+    }
+
     _test_createState(url: string) {
         const b = StateTree.build(this.state.data.tree);
 
@@ -94,23 +118,27 @@ export class PluginContext {
     }
 
     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.canvas3d.add(o.obj.data.repr);
-            this.canvas3d.requestDraw(true);
+        merge(this.events.state.data.object.created, this.events.state.behavior.object.created).subscribe(o => {
+            console.log('creating', o.obj.type);
+            if (!SO.Behavior.is(o.obj)) return;
+            o.obj.data.register();
+        });
+
+        merge(this.events.state.data.object.removed, this.events.state.behavior.object.removed).subscribe(o => {
+            if (!SO.Behavior.is(o.obj)) return;
+            o.obj.data.unregister();
         });
-        this.state.data.context.events.object.updated.subscribe(o => {
-            const oo = o.obj;
-            if (!SO.StructureRepresentation3D.is(oo)) return;
-            console.log('updating repr', oo.data.repr);
-            this.canvas3d.add(oo.data.repr);
-            this.canvas3d.requestDraw(true);
+
+        merge(this.events.state.data.object.replaced, this.events.state.behavior.object.replaced).subscribe(o => {
+            if (o.oldObj && SO.Behavior.is(o.oldObj)) o.oldObj.data.unregister();
+            if (o.newObj && SO.Behavior.is(o.newObj)) o.newObj.data.register();
         });
     }
 
     _test_centerView() {
-        const sel = StateSelection.select('structure', this.state.data);
+        const sel = StateSelection.select(StateSelection.root().subtree().ofType(SO.Structure.type), this.state.data);
+        if (!sel.length) return;
+
         const center = (sel[0].obj! as SO.Structure).data.boundary.sphere.center;
         console.log({ sel, center, rc: this.canvas3d.reprCount });
         this.canvas3d.center(center);
@@ -135,6 +163,8 @@ export class PluginContext {
 
     constructor() {
         this.initEvents();
+
+        this._test_initBehaviours();
     }
 
     // logger = ;

+ 26 - 18
src/mol-plugin/state.ts

@@ -12,41 +12,47 @@ export { PluginState }
 
 class PluginState {
     readonly data: State;
-    readonly behaviour: State;
-
-    readonly canvas = {
-        camera: CombinedCamera.create()
-    };
+    readonly behavior: State;
 
     getSnapshot(): PluginState.Snapshot {
         return {
             data: this.data.getSnapshot(),
-            behaviour: this.behaviour.getSnapshot(),
-            canvas: {
-                camera: { ...this.canvas.camera }
+            behaviour: this.behavior.getSnapshot(),
+            canvas3d: {
+                camera: { ...this.plugin.canvas3d.camera }
             }
         };
     }
 
     setSnapshot(snapshot: PluginState.Snapshot) {
-        // TODO events
-        this.behaviour.setSnapshot(snapshot.behaviour);
+        this.behavior.setSnapshot(snapshot.behaviour);
         this.data.setSnapshot(snapshot.data);
-        this.canvas.camera = { ...snapshot.canvas.camera };
+
+        // TODO: handle camera
+        // console.log({ old: { ...this.plugin.canvas3d.camera  }, new: snapshot.canvas3d.camera });
+        // CombinedCamera.copy(snapshot.canvas3d.camera, this.plugin.canvas3d.camera);
+        // CombinedCamera.update(this.plugin.canvas3d.camera);
+        // this.plugin.canvas3d.center
+        // console.log({ copied: { ...this.plugin.canvas3d.camera  } });
+        // this.plugin.canvas3d.requestDraw(true);
+        // console.log('updated camera');
+    }
+
+    updateData(tree: StateTree) {
+        return this.plugin.runTask(this.data.update(tree));
     }
 
-    async updateData(tree: StateTree) {
-        // TODO: "task observer"
-        await this.data.update(tree).run(p => console.log(p), 250);
+    updateBehaviour(tree: StateTree) {
+        return this.plugin.runTask(this.behavior.update(tree));
     }
 
     dispose() {
         this.data.dispose();
     }
 
-    constructor(globalContext: unknown) {
-        this.data = State.create(new SO.Root({ label: 'Root' }, { }), { globalContext });
-        this.behaviour = State.create(new SO.Root({ label: 'Root' }, { }), { globalContext });
+    constructor(private plugin: import('./context').PluginContext) {
+        this.data = State.create(new SO.DataRoot({ label: 'Root' }, { }), { globalContext: plugin });
+        this.behavior = State.create(new SO.BehaviorRoot({ label: 'Root' }, { }), { globalContext: plugin });
     }
 }
 
@@ -54,6 +60,8 @@ namespace PluginState {
     export interface Snapshot {
         data: State.Snapshot,
         behaviour: State.Snapshot,
-        canvas: PluginState['canvas']
+        canvas3d: {
+            camera: CombinedCamera
+        }
     }
 }

+ 3 - 1
src/mol-plugin/state/objects.ts

@@ -12,7 +12,9 @@ import { StructureRepresentation } from 'mol-repr/structure/index';
 const _create = PluginStateObject.Create
 
 namespace PluginStateObjects {
-    export class Root extends _create({ name: 'Root', shortName: 'R', typeClass: 'Root', description: 'Where everything begins.' }) { }
+    export class DataRoot extends _create({ name: 'Root', shortName: 'R', typeClass: 'Root', description: 'Where everything begins.' }) { }
+    export class BehaviorRoot 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 class Behavior extends _create<import('../behavior').PluginBehavior>({ name: 'Behavior', shortName: 'B', typeClass: 'Behavior', description: 'Modifies plugin functionality.' }) { }

+ 5 - 2
src/mol-plugin/state/transforms/data.ts

@@ -13,15 +13,18 @@ import { ParamDefinition as PD } from 'mol-util/param-definition';
 
 export { Download }
 namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } }
-const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({
+const Download = PluginStateTransform.Create<SO.DataRoot, SO.Data.String | SO.Data.Binary, Download.Params>({
     name: 'download',
     display: {
         name: 'Download',
         description: 'Download string or binary data from the specified URL'
     },
-    from: [SO.Root],
+    from: [SO.DataRoot],
     to: [SO.Data.String, SO.Data.Binary],
     params: {
+        default: () => ({
+            url: 'https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif'
+        }),
         controls: () => ({
             url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''),
             isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false)

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

@@ -15,6 +15,7 @@ export { CreateStructureRepresentation }
 namespace CreateStructureRepresentation { export interface Params { } }
 const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, CreateStructureRepresentation.Params>({
     name: 'create-structure-representation',
+    display: { name: 'Create 3D Representation' },
     from: [SO.Structure],
     to: [SO.StructureRepresentation3D],
     apply({ a, params }) {

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

@@ -6,6 +6,8 @@
 
 import * as React from 'react';
 import { PluginContext } from '../context';
+import { Transform, Transformer, StateObject } from 'mol-state';
+import { ParametersComponent } from 'mol-app/component/parameters';
 
 export class Controls extends React.Component<{ plugin: PluginContext }, { id: string }> {
     state = { id: '1grm' };
@@ -16,6 +18,16 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s
         this.props.plugin._test_createState(url);
     }
 
+    private _snap:any = void 0;
+    private getSnapshot = () => {
+        this._snap = this.props.plugin.state.getSnapshot();
+        console.log(btoa(JSON.stringify(this._snap)));
+    }
+    private setSnapshot = () => {
+        if (!this._snap) return;
+        this.props.plugin.state.setSnapshot(this._snap);
+    }
+
     render() {
         return <div>
             <input type='text' defaultValue={this.state.id} onChange={e => this.setState({ id: e.currentTarget.value })} />
@@ -23,6 +35,55 @@ export class Controls extends React.Component<{ plugin: PluginContext }, { id: s
             <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/>
+            <hr />
+            <button onClick={this.getSnapshot}>Get Snapshot</button>
+            <button onClick={this.setSnapshot}>Set Snapshot</button>
         </div>;
     }
+}
+
+export class _test_CreateTransform extends React.Component<{ plugin: PluginContext, nodeRef: Transform.Ref, transformer: Transformer }, { params: any }> {
+    private getObj() {
+        const obj = this.props.plugin.state.data.objects.get(this.props.nodeRef)!;
+        return obj;
+    }
+
+    private getDefaultParams() {
+        const p = this.props.transformer.definition.params;
+        if (!p || !p.default) return { };
+        const obj = this.getObj();
+        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 obj = this.getObj();
+        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._test_applyTransform(this.props.nodeRef, this.props.transformer, this.state.params);
+    }
+
+    state = { params: this.getDefaultParams() }
+
+    render() {
+        const obj = this.getObj();
+        if (obj.state !== StateObject.StateType.Ok) {
+            // TODO filter this elsewhere
+            return <div />;
+        }
+
+        const t = this.props.transformer;
+
+        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>
+            <ParametersComponent params={this.getParamDef()} values={this.state.params as any} onChange={params => this.setState({ params })} />
+            <button onClick={() => this.create()} style={{ width: '100%' }}>Create</button>
+        </div>
+    }
 }

+ 34 - 5
src/mol-plugin/ui/plugin.tsx

@@ -6,17 +6,22 @@
 
 import * as React from 'react';
 import { PluginContext } from '../context';
-import { Tree } from './tree';
+import { StateTree } from './state-tree';
 import { Viewport } from './viewport';
-import { Controls } from './controls';
+import { Controls, _test_CreateTransform } from './controls';
+import { Transformer } from 'mol-state';
+
+// TODO: base object with subscribe helpers
 
 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 style={{ position: 'absolute', width: '350px', height: '100%', overflowY: 'scroll' }}>
+                <StateTree plugin={this.props.plugin} />
+                <hr />
+                <_test_CurrentObject plugin={this.props.plugin} />
             </div>
-            <div style={{ position: 'absolute', left: '250px', right: '250px', height: '100%' }}>
+            <div style={{ position: 'absolute', left: '350px', right: '250px', height: '100%' }}>
                 <Viewport plugin={this.props.plugin} />
             </div>
             <div style={{ position: 'absolute', width: '250px', right: '0', height: '100%' }}>
@@ -24,4 +29,28 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, { }> {
             </div>
         </div>;
     }
+}
+
+export class _test_CurrentObject extends React.Component<{ plugin: PluginContext }, { }> {
+    componentWillMount() {
+        this.props.plugin.behaviors.state.data.currentObject.subscribe(() => this.forceUpdate());
+    }
+    render() {
+        const ref = this.props.plugin.behaviors.state.data.currentObject.value.ref;
+        // const n = this.props.plugin.state.data.tree.nodes.get(ref)!;
+        const obj = this.props.plugin.state.data.objects.get(ref)!;
+
+        const type = obj && obj.obj ? obj.obj.type : void 0;
+
+        const transforms = type
+            ? Transformer.fromType(type)
+            : []
+        return <div>
+            Current Ref: {this.props.plugin.behaviors.state.data.currentObject.value.ref}
+            <hr />
+            {
+                transforms.map((t, i) => <_test_CreateTransform key={`${t.id} ${ref} ${i}`} plugin={this.props.plugin} transformer={t} nodeRef={ref} />)
+            }
+        </div>;
+    }
 }

+ 14 - 8
src/mol-plugin/ui/tree.tsx → src/mol-plugin/ui/state-tree.tsx

@@ -8,21 +8,23 @@ import * as React from 'react';
 import { PluginContext } from '../context';
 import { PluginStateObject } from 'mol-plugin/state/base';
 import { StateObject } from 'mol-state'
+import { PluginCommands } from 'mol-plugin/command';
 
-export class Tree extends React.Component<{ plugin: PluginContext }, { }> {
-
+export class StateTree extends React.Component<{ plugin: PluginContext }, { }> {
     componentWillMount() {
-        this.props.plugin.events.data.updated.subscribe(() => this.forceUpdate());
+        this.props.plugin.events.state.data.updated.subscribe(() => this.forceUpdate());
     }
     render() {
-        const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
+        // const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;
+        const n = this.props.plugin.state.data.tree.rootRef;
         return <div>
-            {n.children.map(c => <TreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}
+            <StateTreeNode plugin={this.props.plugin} nodeRef={n} key={n} />
+            { /* n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />) */}
         </div>;
     }
 }
 
-export class TreeNode extends React.Component<{ plugin: PluginContext, nodeRef: string }, { }> {
+export class StateTreeNode 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)!;
@@ -33,10 +35,14 @@ export class TreeNode extends React.Component<{ plugin: PluginContext, nodeRef:
         }
         const props = obj.obj!.props as PluginStateObject.Props;
         return <div style={{ borderLeft: '1px solid black', paddingLeft: '5px' }}>
-            {props.label} {props.description ? <small>{props.description}</small> : void 0}
+            <a href='#' onClick={e => {
+                e.preventDefault();
+                PluginCommands.Data.SetCurrentObject.dispatch(this.props.plugin, { ref: this.props.nodeRef });
+            }}>{props.label}</a>
+            {props.description ? <small>{props.description}</small> : void 0}
             {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 style={{ marginLeft: '10px' }}>{n.children.map(c => <StateTreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}</div>
             }
         </div>;
     }

+ 4 - 3
src/mol-state/state.ts

@@ -43,8 +43,8 @@ class State {
 
     setSnapshot(snapshot: State.Snapshot): void {
         const tree = StateTree.fromJSON(snapshot.tree);
-        // TODO: support props
-        this.update(tree);
+        // TODO: support props and async
+        this.update(tree).run();
     }
 
     setCurrent(ref: Transform.Ref) {
@@ -128,9 +128,10 @@ namespace State {
         const roots = findUpdateRoots(ctx.objects, ctx.tree);
         const deletes = findDeletes(ctx);
         for (const d of deletes) {
+            const obj = ctx.objects.has(d) ? ctx.objects.get(d)!.obj : void 0;
             ctx.objects.delete(d);
             ctx.transformCache.delete(d);
-            ctx.stateCtx.events.object.removed.next({ ref: d });
+            ctx.stateCtx.events.object.removed.next({ ref: d, obj });
             // TODO: handle current object change
         }
 

+ 6 - 4
src/mol-state/tree.ts

@@ -52,8 +52,8 @@ namespace StateTree {
 
         export class Root implements Builder {
             private state: State;
-            to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref); }
-            toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.rootRef as any); }
+            to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); }
+            toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.rootRef as any, this); }
             delete(ref: Transform.Ref) { this.state.tree.remove(ref); return this; }
             getTree(): StateTree { return this.state.tree.asImmutable(); }
             constructor(tree: StateTree) { this.state = { tree: ImmutableTree.asTransient(tree) } }
@@ -63,12 +63,14 @@ namespace StateTree {
             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);
+                return new To(this.state, t.ref, this.root);
             }
 
+            and() { return this.root; }
+
             getTree(): StateTree { return this.state.tree.asImmutable(); }
 
-            constructor(private state: State, private ref: Transform.Ref) {
+            constructor(private state: State, private ref: Transform.Ref, private root: Root) {
                 if (!this.state.tree.nodes.has(ref)) {
                     throw new Error(`Could not find node '${ref}'.`);
                 }