From d120039ef17a4647c5a304accd0d99fa6985b403 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Tue, 6 Nov 2018 13:22:14 +0100 Subject: [PATCH] wip state & plugin --- src/mol-plugin/behaviour.ts | 21 +++++++++++++ src/mol-plugin/command.ts | 2 ++ src/mol-plugin/context.ts | 9 ++++++ src/mol-plugin/state/transforms/data.ts | 25 ++++++++++++---- src/mol-plugin/state/transforms/model.ts | 20 ++++++++++--- src/mol-plugin/state/transforms/visuals.ts | 5 ++-- src/mol-state/object.ts | 3 +- src/mol-state/state.ts | 33 +++++++++++++------- src/mol-state/transformer.ts | 35 ++++++++++++---------- 9 files changed, 114 insertions(+), 39 deletions(-) create mode 100644 src/mol-plugin/command.ts diff --git a/src/mol-plugin/behaviour.ts b/src/mol-plugin/behaviour.ts index e69de29bb..e8025a76a 100644 --- a/src/mol-plugin/behaviour.ts +++ b/src/mol-plugin/behaviour.ts @@ -0,0 +1,21 @@ +/** + * 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 new file mode 100644 index 000000000..452c8ac07 --- /dev/null +++ b/src/mol-plugin/command.ts @@ -0,0 +1,2 @@ +// 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 4b2fc4a1c..c5e7d6846 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -35,6 +35,15 @@ export class PluginContext { } } + /** + * This should be used in all transform related request so that it could be "spoofed" to allow + * "static" access to resources. + */ + async fetch(url: string, type: 'string' | 'binary' = 'string'): Promise<string | Uint8Array> { + const req = await fetch(url); + return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer()); + } + dispose() { if (this.disposed) return; this.canvas3d.dispose(); diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index e405712b1..6ac2fa935 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -8,24 +8,37 @@ import { PluginStateTransform } from '../base'; import { PluginStateObjects as SO } from '../objects'; import { Task } from 'mol-task'; import CIF from 'mol-io/reader/cif' +import { PluginContext } from 'mol-plugin/context'; -export const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, { url: string, isBinary?: boolean, label?: string }>({ +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>({ name: 'download', + display: { + name: 'Download', + description: 'Download string or binary data from the specified URL' + }, from: [SO.Root], to: [SO.Data.String, SO.Data.Binary], - apply({ params: p }) { + apply({ params: p }, globalCtx: PluginContext) { return Task.create('Download', async ctx => { // TODO: track progress - const req = await fetch(p.url); + const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string'); return p.isBinary - ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, new Uint8Array(await req.arrayBuffer())) - : new SO.Data.String({ label: p.label ? p.label : p.url }, await req.text()); + ? new SO.Data.Binary({ label: p.label ? p.label : p.url }, data as Uint8Array) + : new SO.Data.String({ label: p.label ? p.label : p.url }, data as string); }); } }); -export const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, { }>({ +export { ParseCif } +namespace ParseCif { export interface Params { } } +const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({ name: 'parse-cif', + display: { + name: 'Parse CIF', + description: 'Parse CIF from String or Binary data' + }, from: [SO.Data.String, SO.Data.Binary], to: [SO.Data.Cif], apply({ a }) { diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 419894e9d..e75e0455b 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -9,11 +9,17 @@ import { PluginStateObjects as SO } from '../objects'; import { Task } from 'mol-task'; import { Model, Format, Structure } from 'mol-model/structure'; -export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, { blockHeader?: string }>({ +export { CreateModelsFromMmCif } +namespace CreateModelsFromMmCif { export interface Params { blockHeader?: string } } +const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, CreateModelsFromMmCif.Params>({ name: 'create-models-from-mmcif', + display: { + name: 'Models from mmCIF', + description: 'Identify and create all separate models in the specified CIF data block' + }, from: [SO.Data.Cif], to: [SO.Models], - defaultParams: a => ({ blockHeader: a.data.blocks[0].header }), + params: { default: a => ({ blockHeader: a.data.blocks[0].header }) }, apply({ a, params }) { return Task.create('Parse mmCIF', async ctx => { const header = params.blockHeader || a.data.blocks[0].header; @@ -27,11 +33,17 @@ export const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO } }); -export const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, { modelIndex: number }>({ +export { CreateStructureFromModel } +namespace CreateStructureFromModel { export interface Params { modelIndex: number } } +const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, CreateStructureFromModel.Params>({ name: 'structure-from-model', + display: { + name: 'Structure from Model', + description: 'Create a molecular structure from the specified model.' + }, from: [SO.Models], to: [SO.Structure], - defaultParams: () => ({ modelIndex: 0 }), + params: { default: () => ({ modelIndex: 0 }) }, apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); // TODO: make Structure.ofModel async? diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts index f03e8bdc2..80e65bd79 100644 --- a/src/mol-plugin/state/transforms/visuals.ts +++ b/src/mol-plugin/state/transforms/visuals.ts @@ -10,11 +10,12 @@ import { PluginStateTransform } from '../base'; import { PluginStateObjects as SO } from '../objects'; import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon'; -export const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, { }>({ +export { CreateStructureRepresentation } +namespace CreateStructureRepresentation { export interface Params { } } +const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, SO.StructureRepresentation3D, CreateStructureRepresentation.Params>({ name: 'create-structure-representation', from: [SO.Structure], to: [SO.StructureRepresentation3D], - defaultParams: () => ({ modelIndex: 0 }), apply({ a, params }) { return Task.create('Structure Representation', async ctx => { const repr = CartoonRepresentation(); diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index aed685c1a..dddfcb68b 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -9,7 +9,7 @@ import { Transform } from './transform'; import { UUID } from 'mol-util'; /** A mutable state object */ -export interface StateObject<P = unknown, D = unknown> { +export interface StateObject<P = any, D = any> { readonly id: UUID, readonly type: StateObject.Type, readonly props: P, @@ -45,7 +45,6 @@ export namespace StateObject { static is(obj?: StateObject): obj is StateObject<Props, Data> { return !!obj && dataType === obj.type; } id = UUID.create(); type = dataType; - ref = 'not set' as Transform.Ref; constructor(public props: Props, public data: Data) { } } } diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index d59f98ce0..0a4932e30 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -17,6 +17,8 @@ export { State } class State { private _tree: StateTree = StateTree.create(); + private transformCache = new Map<Transform.Ref, unknown>(); + get tree() { return this._tree; } readonly objects: State.Objects = new Map(); @@ -44,7 +46,8 @@ class State { taskCtx, oldTree, tree: tree, - objects: this.objects + objects: this.objects, + transformCache: this.transformCache }; // TODO: have "cancelled" error? Or would this be handled automatically? return update(ctx); @@ -71,7 +74,7 @@ class State { } } -namespace State { +namespace State { export type Objects = Map<Transform.Ref, StateObject.Node> export interface Snapshot { @@ -91,7 +94,8 @@ namespace State { taskCtx: RuntimeContext, oldTree: StateTree, tree: StateTree, - objects: State.Objects + objects: State.Objects, + transformCache: Map<Ref, unknown> } async function update(ctx: UpdateContext) { @@ -99,6 +103,7 @@ namespace State { const deletes = findDeletes(ctx); for (const d of deletes) { ctx.objects.delete(d); + ctx.transformCache.delete(d); ctx.stateCtx.events.object.removed.next({ ref: d }); } @@ -174,6 +179,7 @@ namespace State { const wrap = ctx.objects.get(ref)!; if (wrap.obj) { ctx.stateCtx.events.object.removed.next({ ref }); + ctx.transformCache.delete(ref); wrap.obj = void 0; } @@ -230,7 +236,7 @@ namespace State { // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined') if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) { // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); - const obj = await createObject(ctx, transform.transformer, parent, transform.params); + const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); objects.set(currentRef, { ref: currentRef, obj, @@ -243,9 +249,9 @@ namespace State { // console.log('updating...', transform.transformer.id); const current = objects.get(currentRef)!; const oldParams = oldTree.getValue(currentRef)!.params; - switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) { + switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) { case Transformer.UpdateResult.Recreate: { - const obj = await createObject(ctx, transform.transformer, parent, transform.params); + const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params); objects.set(currentRef, { ref: currentRef, obj, @@ -271,13 +277,20 @@ namespace State { return t as T; } - function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) { - return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx); + function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) { + const cache = { }; + ctx.transformCache.set(ref, cache); + return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx); } - async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { + async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { if (!transformer.definition.update) { return Transformer.UpdateResult.Recreate; } - return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx); + let cache = ctx.transformCache.get(ref); + if (!cache) { + cache = { }; + ctx.transformCache.set(ref, cache); + } + return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx); } \ No newline at end of file diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 972d21833..6b9e0c7ab 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -7,6 +7,7 @@ import { Task } from 'mol-task'; import { StateObject } from './object'; import { Transform } from './transform'; +import { Param } from 'mol-util/parameter'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { apply(params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, @@ -19,18 +20,22 @@ 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<Props> = { [P in keyof Props]?: any } + export type ControlsFor<A extends StateObject, Props> = { [P in keyof Props]?: ((a: A, globalCtx: unknown) => Param) } export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { a: A, - params: P + params: P, + /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */ + cache: unknown } export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { a: A, b: B, oldParams: P, - newParams: P + newParams: P, + /** A cache object that is purged each time the corresponding StateObject is removed or recreated. */ + cache: unknown } export enum UpdateResult { Unchanged, Updated, Recreate } @@ -39,6 +44,7 @@ export namespace Transformer { readonly name: string, readonly from: { type: StateObject.Type }[], readonly to: { type: StateObject.Type }[], + readonly display?: { readonly name: string, readonly description?: string }, /** * Apply the actual transformation. It must be pure (i.e. with no side effects). @@ -53,17 +59,16 @@ export namespace Transformer { */ update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult, - /** Check the parameters and return a list of errors if the are not valid. */ - defaultParams?(a: A, globalCtx: unknown): P, - - /** Specify default control descriptors for the parameters */ - defaultControls?(a: A, globalCtx: unknown): Transformer.ControlsFor<P>, - - /** Check the parameters and return a list of errors if the are not valid. */ - validateParams?(a: A, params: P, globalCtx: unknown): string[] | undefined, - - /** Optional custom parameter equality. Use deep structural equal by default. */ - areParamsEqual?(oldParams: P, newParams: P): boolean, + params?: { + /** 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>, + /** 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. */ + areEqual?(oldParams: P, newParams: P): boolean + } /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean, @@ -75,7 +80,7 @@ export namespace Transformer { customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } - const registry = new Map<Id, Transformer>(); + const registry = new Map<Id, Transformer<any, any>>(); export function get(id: string): Transformer { const t = registry.get(id as Id); -- GitLab