Browse Source

wip, state & plugin

David Sehnal 6 years ago
parent
commit
76bd852de2

+ 117 - 0
src/mol-plugin/command.ts

@@ -1,2 +1,119 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginContext } from './context';
+import { LinkedList } from 'mol-data/generic';
+
+export { PluginCommand }
+
+/** namespace.id must a globally unique identifier */
+function PluginCommand<T>(namespace: string, id: string, params?: PluginCommand.Descriptor<T>['params']): PluginCommand.Descriptor<T> {
+    return new Impl(`${namespace}.${id}` as PluginCommand.Id, params);
+}
+
+const cmdRepo = new Map<string, PluginCommand.Descriptor<any>>();
+class Impl<T> implements PluginCommand.Descriptor<T> {
+    dispatch(ctx: PluginContext, params: T): Promise<void> {
+        return ctx.commands.dispatch(this, params)
+    }
+    constructor(public id: PluginCommand.Id, public params: PluginCommand.Descriptor<T>['params']) {
+        if (cmdRepo.has(id)) throw new Error(`Command id '${id}' already in use.`);
+        cmdRepo.set(id, this);
+    }
+}
+
+namespace PluginCommand {
+    export type Id = string & { '@type': 'plugin-command-id' }
+
+    export interface Descriptor<T = unknown> {
+        readonly id: PluginCommand.Id,
+        dispatch(ctx: PluginContext, params: T): Promise<void>,
+        params?: { toJSON(params: T): any, fromJSON(json: any): T }
+    }
+
+    type Action<T> = (params: T) => void | Promise<void>
+    type Instance = { id: string, params: any, resolve: () => void, reject: (e: any) => void }
+
+    export class Manager {
+        private subs = new Map<string, Action<any>[]>();
+        private queue = LinkedList<Instance>();
+        private disposing = false;
+
+        subscribe<T>(cmd: Descriptor<T>, action: Action<T>) {
+            let actions = this.subs.get(cmd.id);
+            if (!actions) {
+                actions = [];
+                this.subs.set(cmd.id, actions);
+            }
+            actions.push(action);
+
+            return {
+                unsubscribe: () => {
+                    const actions = this.subs.get(cmd.id);
+                    if (!actions) return;
+                    const idx = actions.indexOf(action);
+                    if (idx < 0) return;
+                    for (let i = idx + 1; i < actions.length; i++) {
+                        actions[i - 1] = actions[i];
+                    }
+                    actions.pop();
+                }
+            }
+        }
+
+
+        /** Resolves after all actions have completed */
+        dispatch<T>(cmd: Descriptor<T> | Id, params: T) {
+            return new Promise<void>((resolve, reject) => {
+                if (!this.disposing) {
+                    reject('disposed');
+                    return;
+                }
+
+                const id = typeof cmd === 'string' ? cmd : (cmd as Descriptor<T>).id;
+                const actions = this.subs.get(id);
+                if (!actions) {
+                    resolve();
+                    return;
+                }
+
+                this.queue.addLast({ id, params, resolve, reject });
+                this.next();
+            });
+        }
+
+        dispose() {
+            this.subs.clear();
+            while (this.queue.count > 0) {
+                this.queue.removeFirst();
+            }
+        }
+
+        private async next() {
+            if (this.queue.count === 0) return;
+            const cmd = this.queue.removeFirst()!;
+
+            const actions = this.subs.get(cmd.id);
+            if (!actions) return;
+
+            try {
+                // TODO: should actions be called "asynchronously" ("setImmediate") instead?
+                for (const a of actions) {
+                    await a(cmd.params);
+                }
+                cmd.resolve();
+            } catch (e) {
+                cmd.reject(e);
+            } finally {
+                if (!this.disposing) this.next();
+            }
+        }
+    }
+}
+
+
 // TODO: command interface and queue.
 // How to handle command resolving? Track how many subscriptions a command has?

+ 7 - 10
src/mol-plugin/context.ts

@@ -11,6 +11,7 @@ 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';
 
 export class PluginContext {
     private disposed = false;
@@ -19,11 +20,13 @@ export class PluginContext {
     readonly state = new PluginState(this);
 
     readonly events = {
-        stateUpdated: this.ev<undefined>()
+        data: this.state.data.context.events
     };
 
     readonly canvas3d: Canvas3D;
 
+    readonly commands = new PluginCommand.Manager();
+
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) {
         try {
             (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container);
@@ -47,6 +50,7 @@ export class PluginContext {
 
     dispose() {
         if (this.disposed) return;
+        this.commands.dispose();
         this.canvas3d.dispose();
         this.ev.dispose();
         this.state.dispose();
@@ -77,13 +81,7 @@ export class PluginContext {
             .apply(StateTransforms.Visuals.CreateStructureRepresentation)
             .getTree();
 
-        this._test_updateStateData(newTree);
-    }
-
-    async _test_updateStateData(tree: StateTree) {
-        await this.state.data.update(tree).run(p => console.log(p), 250);
-        console.log(this.state.data);
-        this.events.stateUpdated.next();
+        this.state.updateData(newTree);
     }
 
     private initEvents() {
@@ -113,9 +111,8 @@ export class PluginContext {
     _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);
+        return this.state.updateData(newTree);
         // this.viewer.requestDraw(true);
     }
 

+ 1 - 0
src/mol-plugin/providers/custom-prop.ts

@@ -0,0 +1 @@
+// TODO

+ 1 - 0
src/mol-plugin/providers/theme.ts

@@ -0,0 +1 @@
+// TODO

+ 27 - 6
src/mol-plugin/state.ts

@@ -4,24 +4,40 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State } from 'mol-state';
+import { State, StateTree } from 'mol-state';
 import { PluginStateObjects as SO } from './state/objects';
+import { CombinedCamera } from 'mol-canvas3d/camera/combined';
 
 export { PluginState }
 
 class PluginState {
     readonly data: State;
+    readonly behaviour: State;
+
+    readonly canvas = {
+        camera: CombinedCamera.create()
+    };
 
     getSnapshot(): PluginState.Snapshot {
-        throw 'nyi';
+        return {
+            data: this.data.getSnapshot(),
+            behaviour: this.behaviour.getSnapshot(),
+            canvas: {
+                camera: { ...this.canvas.camera }
+            }
+        };
     }
 
     setSnapshot(snapshot: PluginState.Snapshot) {
-        throw 'nyi';
+        // TODO events
+        this.behaviour.setSnapshot(snapshot.behaviour);
+        this.data.setSnapshot(snapshot.data);
+        this.canvas.camera = { ...snapshot.canvas.camera };
     }
 
-    setDataSnapshot(snapshot: State.Snapshot) {
-        throw 'nyi';
+    async updateData(tree: StateTree) {
+        // TODO: "task observer"
+        await this.data.update(tree).run(p => console.log(p), 250);
     }
 
     dispose() {
@@ -30,9 +46,14 @@ class PluginState {
 
     constructor(globalContext: unknown) {
         this.data = State.create(new SO.Root({ label: 'Root' }, { }), { globalContext });
+        this.behaviour = State.create(new SO.Root({ label: 'Root' }, { }), { globalContext });
     }
 }
 
 namespace PluginState {
-    export interface Snapshot { }
+    export interface Snapshot {
+        data: State.Snapshot,
+        behaviour: State.Snapshot,
+        canvas: PluginState['canvas']
+    }
 }

+ 7 - 0
src/mol-plugin/state/action.ts

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+// TODO actions that modify state and can be "applied" to certain state objects.

+ 7 - 0
src/mol-plugin/state/actions/basic.ts

@@ -0,0 +1,7 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+// TODO: basic actions like "download and create default representation"

+ 1 - 1
src/mol-plugin/ui/tree.tsx

@@ -12,7 +12,7 @@ import { StateObject } from 'mol-state'
 export class Tree extends React.Component<{ plugin: PluginContext }, { }> {
 
     componentWillMount() {
-        this.props.plugin.events.stateUpdated.subscribe(() => this.forceUpdate());
+        this.props.plugin.events.data.updated.subscribe(() => this.forceUpdate());
     }
     render() {
         const n = this.props.plugin.state.data.tree.nodes.get(this.props.plugin.state.data.tree.rootRef)!;

+ 4 - 1
src/mol-state/context.ts

@@ -22,8 +22,11 @@ class StateContext {
             replaced: this.ev<{ ref: Transform.Ref, oldObj?: StateObject, newObj?: StateObject }>(),
             created: this.ev<{ ref: Transform.Ref, obj: StateObject }>(),
             removed: this.ev<{ ref: Transform.Ref, obj?: StateObject }>(),
+
+            currentChanged: this.ev<{ ref: Transform.Ref }>()
         },
-        warn: this.ev<string>()
+        warn: this.ev<string>(),
+        updated: this.ev<void>()
     };
 
     readonly globalContext: unknown;

+ 37 - 17
src/mol-state/state.ts

@@ -17,19 +17,34 @@ export { State }
 
 class State {
     private _tree: StateTree = StateTree.create();
+    private _current: Transform.Ref = this._tree.rootRef;
     private transformCache = new Map<Transform.Ref, unknown>();
 
     get tree() { return this._tree; }
+    get current() { return this._current; }
 
     readonly objects: State.Objects = new Map();
     readonly context: StateContext;
 
     getSnapshot(): State.Snapshot {
-        throw 'nyi';
+        const props = Object.create(null);
+        const keys = this.objects.keys();
+        while (true) {
+            const key = keys.next();
+            if (key.done) break;
+            const o = this.objects.get(key.value)!;
+            props[key.value] = { ...o.props };
+        }
+        return {
+            tree: StateTree.toJSON(this._tree),
+            props
+        };
     }
 
     setSnapshot(snapshot: State.Snapshot): void {
-        throw 'nyi';
+        const tree = StateTree.fromJSON(snapshot.tree);
+        // TODO: support props
+        this.update(tree);
     }
 
     dispose() {
@@ -37,20 +52,25 @@ class State {
     }
 
     update(tree: StateTree): Task<void> {
-        return Task.create('Update Tree', taskCtx => {
-            const oldTree = this._tree;
-            this._tree = tree;
-
-            const ctx: UpdateContext = {
-                stateCtx: this.context,
-                taskCtx,
-                oldTree,
-                tree: tree,
-                objects: this.objects,
-                transformCache: this.transformCache
-            };
-            // TODO: have "cancelled" error? Or would this be handled automatically?
-            return update(ctx);
+        // TODO: support props
+        return Task.create('Update Tree', async taskCtx => {
+            try {
+                const oldTree = this._tree;
+                this._tree = tree;
+
+                const ctx: UpdateContext = {
+                    stateCtx: this.context,
+                    taskCtx,
+                    oldTree,
+                    tree: tree,
+                    objects: this.objects,
+                    transformCache: this.transformCache
+                };
+                // TODO: have "cancelled" error? Or would this be handled automatically?
+                await update(ctx);
+            } finally {
+                this.context.events.updated.next();
+            }
         });
     }
 
@@ -78,7 +98,7 @@ namespace State {
     export type Objects = Map<Transform.Ref, StateObject.Node>
 
     export interface Snapshot {
-        readonly tree: StateTree,
+        readonly tree: StateTree.Serialized,
         readonly props: { [key: string]: unknown }
     }
 

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

@@ -13,6 +13,7 @@ interface StateTree extends ImmutableTree<Transform> { }
 
 namespace StateTree {
     export interface Transient extends ImmutableTree.Transient<Transform> { }
+    export interface Serialized extends ImmutableTree.Serialized { }
 
     function _getRef(t: Transform) { return t.ref; }
 
@@ -29,10 +30,10 @@ namespace StateTree {
     }
 
     export function toJSON(tree: StateTree) {
-        return ImmutableTree.toJSON(tree, Transform.toJSON);
+        return ImmutableTree.toJSON(tree, Transform.toJSON) as Serialized;
     }
 
-    export function fromJSON(data: any): StateTree {
+    export function fromJSON(data: Serialized): StateTree {
         return ImmutableTree.fromJSON(data, _getRef, Transform.fromJSON);
     }