diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..78fdaf11f59a02ae5e05cbae8eee5117b11f343b --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts new file mode 100644 index 0000000000000000000000000000000000000000..08d4c2af524386eb2e8e19fb5b4a33590414debc --- /dev/null +++ b/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) { } + } + } +} \ No newline at end of file diff --git a/src/mol-plugin/behaviour/camera.ts b/src/mol-plugin/behavior/camera.ts similarity index 100% rename from src/mol-plugin/behaviour/camera.ts rename to src/mol-plugin/behavior/camera.ts diff --git a/src/mol-plugin/behavior/data.ts b/src/mol-plugin/behavior/data.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b7e1467893e43b73f766f0615279e4c9c8dbfac --- /dev/null +++ b/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' } +}); \ No newline at end of file diff --git a/src/mol-plugin/behavior/representation.ts b/src/mol-plugin/behavior/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/mol-plugin/behaviour.ts b/src/mol-plugin/behaviour.ts deleted file mode 100644 index e8025a76a798a639d7b774da983712b19f425f11..0000000000000000000000000000000000000000 --- a/src/mol-plugin/behaviour.ts +++ /dev/null @@ -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> - } -} \ No newline at end of file diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index fc26dc2eb51de31776154f2f38a88ac8d271b82c..01d93d78264e78fd3639b75a5f323628eca49946 100644 --- a/src/mol-plugin/command.ts +++ b/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? \ No newline at end of file +export * from './command/command'; +export const PluginCommands = { + Data +} \ No newline at end of file diff --git a/src/mol-plugin/command/command.ts b/src/mol-plugin/command/command.ts new file mode 100644 index 0000000000000000000000000000000000000000..6148919f19c34cc3646dc99453008cdd3ac3f422 --- /dev/null +++ b/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(); + } + } + } +} \ No newline at end of file diff --git a/src/mol-plugin/command/data.ts b/src/mol-plugin/command/data.ts new file mode 100644 index 0000000000000000000000000000000000000000..e74a410c9962ec9fd8f178eb60cc34d28982622e --- /dev/null +++ b/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'); \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 112ecaf907f3d4e42bee635b435dee6ce3657d05..5fb5d42ba7121ad2115f07fad62f63c8a9688df7 100644 --- a/src/mol-plugin/context.ts +++ b/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(); diff --git a/src/mol-plugin/state/base.ts b/src/mol-plugin/state/base.ts index 0c1a22111ce70c20c97f0540fa05ef0ee38a44fc..5b7db3fc0583f5f62c809f6b19e1159632680ec1 100644 --- a/src/mol-plugin/state/base.ts +++ b/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 } diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index c5082458aeec58b855edbfa6d5e81a7e526e4c30..53b57be5a948c13ec11d82d249b7bb5be6e1dbe2 100644 --- a/src/mol-plugin/state/objects.ts +++ b/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.' }) { } diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts index 35a01880f38b954301c8a1e0cbc4357fed94c273..ff471db4eaa6786100ec01ddbbdf5dd3843ab0e7 100644 --- a/src/mol-state/context.ts +++ b/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 }); } } \ No newline at end of file diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 384274d2f92767aabe2d787f42aa86f3a8d3849f..5f5e411892741dff2953b3553553c14eb4622412 100644 --- a/src/mol-state/state.ts +++ b/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(); } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 754e2728d61b4c799b52831af01a285a5b2edf32..c6b2129d5f0e967031181570bd60fbb72a8e6161 100644 --- a/src/mol-state/transformer.ts +++ b/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. */