diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 452c8ac0785b3f2af494138967c4811e5e6001b6..2ab28baf362f7c4b08248198d82bfb552e70fd31 100644 --- a/src/mol-plugin/command.ts +++ b/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? \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 1f51130de16838fac9d508b56e5f394bcba8fcdf..7cd0967c1ebbde5ecb097e1d2526addc55a86ee4 100644 --- a/src/mol-plugin/context.ts +++ b/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); } diff --git a/src/mol-plugin/providers/custom-prop.ts b/src/mol-plugin/providers/custom-prop.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ffdd02fcbce683e436c0030ffe0517135c6ceda --- /dev/null +++ b/src/mol-plugin/providers/custom-prop.ts @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/src/mol-plugin/providers/theme.ts b/src/mol-plugin/providers/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ffdd02fcbce683e436c0030ffe0517135c6ceda --- /dev/null +++ b/src/mol-plugin/providers/theme.ts @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index 2d6b40f2045b53c9edb207b7afe2e116b993c086..b9de2611e58498780a885a253172f2b08651c91f 100644 --- a/src/mol-plugin/state.ts +++ b/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'] + } } diff --git a/src/mol-plugin/state/action.ts b/src/mol-plugin/state/action.ts new file mode 100644 index 0000000000000000000000000000000000000000..79be247dea90d8ee04439378c5ac1b6280a20145 --- /dev/null +++ b/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. \ No newline at end of file diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts new file mode 100644 index 0000000000000000000000000000000000000000..11efaeae59cd1b93481163252f7925f02a1372a5 --- /dev/null +++ b/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" \ No newline at end of file diff --git a/src/mol-plugin/ui/tree.tsx b/src/mol-plugin/ui/tree.tsx index 4b1a03e2f1302da71d3c58b1886eaf809c25b6ef..c5ded6963a737d43649a57418881c2c9d6b687ae 100644 --- a/src/mol-plugin/ui/tree.tsx +++ b/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)!; diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts index ff9acff79b16b823acbc9587e105abb89de3167c..acbf366c6003967b50114a7823f940c77f7c73f9 100644 --- a/src/mol-state/context.ts +++ b/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; diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index dacc19a795fb49679a552c7e94ee3b53073fc8ca..2f10a47415cfee67f0447961121b35e3b330b7e0 100644 --- a/src/mol-state/state.ts +++ b/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 } } diff --git a/src/mol-state/tree.ts b/src/mol-state/tree.ts index 4656b98f6367397d42f07e45e08e696ee2a45b58..b8e569a2852a04892f42a7974182c31851f0456c 100644 --- a/src/mol-state/tree.ts +++ b/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); }