Browse Source

wip: plugin

David Sehnal 6 years ago
parent
commit
ce331e480b

+ 12 - 0
src/mol-plugin/behavior.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+export * from './behavior/behavior'
+import * as Data from './behavior/data'
+
+export const PluginBehaviors = {
+    Data
+}

+ 69 - 0
src/mol-plugin/behavior/behavior.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateTransform } from '../state/base';
+import { PluginStateObjects as SO } from '../state/objects';
+import { Transformer } from 'mol-state';
+import { Task } from 'mol-task';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginCommand } from '../command';
+
+export { PluginBehavior }
+
+interface PluginBehavior<P = unknown> {
+    register(): void,
+    unregister(): void,
+
+    /** Update params in place. Optionally return a promise if it depends on an async action. */
+    update?(params: P): boolean | Promise<boolean>
+}
+
+namespace PluginBehavior {
+    export interface Ctor<P = undefined> { new(ctx: PluginContext, params?: P): PluginBehavior<P> }
+
+    export interface CreateParams<P> {
+        name: string,
+        ctor: Ctor<P>,
+        label?: (params: P) => { label: string, description?: string },
+        display: { name: string, description?: string },
+        params?: Transformer.Definition<SO.Root, SO.Behavior, P>['params']
+    }
+
+    export function create<P>(params: CreateParams<P>) {
+        return PluginStateTransform.Create<SO.Root, SO.Behavior, P>({
+            name: params.name,
+            display: params.display,
+            from: [SO.Root],
+            to: [SO.Behavior],
+            params: params.params,
+            apply({ params: p }, ctx: PluginContext) {
+                const label = params.label ? params.label(p) : { label: params.display.name, description: params.display.description };
+                return new SO.Behavior(label, new params.ctor(ctx, p));
+            },
+            update({ b, newParams }) {
+                return Task.create('Update Behavior', async () => {
+                    if (!b.data.update) return Transformer.UpdateResult.Unchanged;
+                    const updated = await b.data.update(newParams);
+                    return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged;
+                })
+            }
+        });
+    }
+
+    export function commandHandler<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 {
+                this.sub = cmd.subscribe(this.ctx, data => action(data, this.ctx));
+            }
+            unregister(): void {
+                if (this.sub) this.sub.unsubscribe();
+                this.sub = void 0;
+            }
+            constructor(private ctx: PluginContext) { }
+        }
+    }
+}

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


+ 35 - 0
src/mol-plugin/behavior/data.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 { 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)),
+    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))),
+    display: { name: 'Update Data Handler' }
+});

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


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

@@ -1,21 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-export { PluginBehaviour }
-
-interface PluginBehaviour<P> {
-    register(): void,
-    unregister(): void,
-
-    /** Update params in place. Optionally return a promise if it depends on an async action. */
-    update(params: P): void | Promise<void>
-}
-
-namespace PluginBehaviour {
-    export interface Ctor<P> {
-        create(params: P): PluginBehaviour<P>
-    }
-}

+ 5 - 123
src/mol-plugin/command.ts

@@ -4,127 +4,9 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { PluginContext } from './context';
-import { LinkedList } from 'mol-data/generic';
-import { RxEventHelper } from 'mol-util/rx-event-helper';
+import * as Data from './command/data';
 
-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;
-
-        private ev = RxEventHelper.create();
-
-        readonly behaviour = {
-            locked: this.ev.behavior<boolean>(false)
-        };
-
-        lock(locked: boolean = true) {
-            this.behaviour.locked.next(locked);
-        }
-
-        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?
+export * from './command/command';
+export const PluginCommands = {
+    Data
+}

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

@@ -0,0 +1,134 @@
+/**
+ * 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';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
+
+export { PluginCommand }
+
+interface PluginCommand<T = unknown> {
+    readonly id: PluginCommand.Id,
+    dispatch(ctx: PluginContext, params: T): Promise<void>,
+    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription,
+    params?: { toJSON(params: T): any, fromJSON(json: any): T }
+}
+
+/** namespace.id must a globally unique identifier */
+function PluginCommand<T>(namespace: string, id: string, params?: PluginCommand<T>['params']): PluginCommand<T> {
+    return new Impl(`${namespace}.${id}` as PluginCommand.Id, params);
+}
+
+const cmdRepo = new Map<string, PluginCommand<any>>();
+class Impl<T> implements PluginCommand<T> {
+    dispatch(ctx: PluginContext, params: T): Promise<void> {
+        return ctx.commands.dispatch(this, params)
+    }
+    subscribe(ctx: PluginContext, action: PluginCommand.Action<T>): PluginCommand.Subscription {
+        return ctx.commands.subscribe(this, action);
+    }
+    constructor(public id: PluginCommand.Id, public params: PluginCommand<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 Subscription {
+        unsubscribe(): void
+    }
+
+    export 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;
+
+        private ev = RxEventHelper.create();
+
+        readonly behaviour = {
+            locked: this.ev.behavior<boolean>(false)
+        };
+
+        lock(locked: boolean = true) {
+            this.behaviour.locked.next(locked);
+        }
+
+        subscribe<T>(cmd: PluginCommand<T>, action: Action<T>): Subscription {
+            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: PluginCommand<T> | Id, params: T) {
+            return new Promise<void>((resolve, reject) => {
+                if (!this.disposing) {
+                    reject('disposed');
+                    return;
+                }
+
+                const id = typeof cmd === 'string' ? cmd : (cmd as PluginCommand<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();
+            }
+        }
+    }
+}

+ 13 - 0
src/mol-plugin/command/data.ts

@@ -0,0 +1,13 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginCommand } from './command';
+import { Transform, StateTree, Transformer } from 'mol-state';
+
+export const SetCurrentObject = PluginCommand<{ ref: Transform.Ref }>('ms-data', 'set-current-object');
+export const Update = PluginCommand<{ tree: StateTree }>('ms-data', 'update');
+export const UpdateObject = PluginCommand<{ ref: Transform.Ref, params: any }>('ms-data', 'update-object');
+export const CreateObject = PluginCommand<{ parentRef?: Transform.Ref, transformer: Transformer, params: any }>('ms-data', 'create-object');

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

@@ -12,6 +12,7 @@ import { RxEventHelper } from 'mol-util/rx-event-helper';
 import { PluginState } from './state';
 import { MolScriptBuilder } from 'mol-script/language/builder';
 import { PluginCommand } from './command';
+import { Task } from 'mol-task';
 
 export class PluginContext {
     private disposed = false;
@@ -52,6 +53,10 @@ export class PluginContext {
         return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer());
     }
 
+    async runTask<T>(task: Task<T>) {
+        return await task.run(p => console.log(p), 250);
+    }
+
     dispose() {
         if (this.disposed) return;
         this.commands.dispose();

+ 1 - 1
src/mol-plugin/state/base.ts

@@ -9,7 +9,7 @@ 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 type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behavior'
     export interface TypeInfo { name: string, shortName: string, description: string, typeClass: TypeClass }
     export interface Props { label: string, description?: string }
 

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

@@ -15,6 +15,8 @@ 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 class Behavior extends _create<import('../behavior').PluginBehavior>({ name: 'Behavior', shortName: 'B', typeClass: 'Behavior', description: 'Modifies plugin functionality.' }) { }
+
     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.' }) { }

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

@@ -29,7 +29,7 @@ class StateContext {
         updated: this.ev<void>()
     };
 
-    readonly behaviours = {
+    readonly behaviors = {
         currentObject: this.ev.behavior<{ ref: Transform.Ref }>(void 0 as any)
     };
 
@@ -43,6 +43,6 @@ class StateContext {
     constructor(params: { globalContext: unknown, defaultObjectProps: unknown, rootRef: Transform.Ref }) {
         this.globalContext = params.globalContext;
         this.defaultObjectProps = params.defaultObjectProps;
-        this.behaviours.currentObject.next({ ref: params.rootRef });
+        this.behaviors.currentObject.next({ ref: params.rootRef });
     }
 }

+ 5 - 0
src/mol-state/state.ts

@@ -47,6 +47,11 @@ class State {
         this.update(tree);
     }
 
+    setCurrent(ref: Transform.Ref) {
+        this._current = ref;
+        this.context.behaviors.currentObject.next({ ref });
+    }
+
     dispose() {
         this.context.dispose();
     }

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

@@ -20,7 +20,7 @@ export namespace Transformer {
     export type Id = string & { '@type': 'transformer-id' }
     export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown;
     export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
-    export type ControlsFor<A extends StateObject, Props> = { [P in keyof Props]?: PD.Any }
+    export type ControlsFor<Props> = { [P in keyof Props]?: PD.Any }
 
     export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
         a: A,
@@ -63,7 +63,7 @@ export namespace Transformer {
             /** Check the parameters and return a list of errors if the are not valid. */
             default?(a: A, globalCtx: unknown): P,
             /** Specify default control descriptors for the parameters */
-            controls?(a: A, globalCtx: unknown): ControlsFor<A, P>,
+            controls?(a: A, globalCtx: unknown): ControlsFor<P>,
             /** Check the parameters and return a list of errors if the are not valid. */
             validate?(a: A, params: P, globalCtx: unknown): string[] | undefined,
             /** Optional custom parameter equality. Use deep structural equal by default. */