diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index d102d1151fe90875636e21d767e7c13ec4323057..17f128c76354494119813d5354c774e83860a6bd 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -12,7 +12,7 @@ import { StateSelection } from 'mol-state/state/selection'; import { CartoonParams } from 'mol-repr/structure/representation/cartoon'; import { BallAndStickParams } from 'mol-repr/structure/representation/ball-and-stick'; import { Download } from '../transforms/data'; -import { StateTree } from 'mol-state'; +import { StateTree, Transformer } from 'mol-state'; import { StateTreeBuilder } from 'mol-state/tree/builder'; import { PolymerIdColorThemeParams } from 'mol-theme/color/polymer-id'; import { UniformSizeThemeParams } from 'mol-theme/size/uniform'; @@ -43,7 +43,7 @@ namespace ObtainStructureHelpers { ]; export function getControls(key: string) { return (ControlMap as any)[key]; } - export function getUrl(src: DownloadStructure.Source): Download.Params { + export function getUrl(src: DownloadStructure.Source): Transformer.Params<Download> { switch (src.name) { case 'url': return src.params; case 'pdbe-updated': return { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params}` }; diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index 24347d8c0a1d1856036b4ee43286a3b9aaa5218f..1cb5a573c0ceec42d0aa99cd310f9c5df12f3f25 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -70,4 +70,5 @@ export namespace PluginStateObject { export namespace PluginStateTransform { export const Create = Transformer.factory('ms-plugin'); + export const BuiltIn = Transformer.factory1('ms-plugin'); } \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index cb7169b557f979fe3aa53493077f454ec2611104..359913cc0a59ebd596136c700cc4ab8c3b1a7f71 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -14,20 +14,21 @@ import { Transformer } from 'mol-state'; import { readFromFile } from 'mol-util/data-source'; 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>({ +type Download = typeof Download +const Download = PluginStateTransform.BuiltIn({ name: 'download', + from: [SO.Root], + to: [SO.Data.String, SO.Data.Binary], + params: { + url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }), + label: PD.makeOptional(PD.Text('')), + isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' })) + } +})({ display: { name: 'Download', description: 'Download string or binary data from the specified URL' }, - from: [SO.Root], - to: [SO.Data.String, SO.Data.Binary], - params: () => ({ - url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }), - label: PD.Text('', { isOptional: true }), - isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)', isOptional: true }) - }), apply({ params: p }, globalCtx: PluginContext) { return Task.create('Download', async ctx => { const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string').runInContext(ctx); @@ -47,21 +48,22 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B }); export { ReadFile } -namespace ReadFile { export interface Params { file: File, isBinary?: boolean, label?: string } } -const ReadFile = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, ReadFile.Params>({ +type ReadFile = typeof ReadFile +const ReadFile = PluginStateTransform.BuiltIn({ name: 'read-file', + from: SO.Root, + to: [SO.Data.String, SO.Data.Binary], + params: { + file: PD.File(), + label: PD.makeOptional(PD.Text('')), + isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' })) + } +})({ display: { name: 'Read File', description: 'Read string or binary data from the specified file' }, - from: [SO.Root], - to: [SO.Data.String, SO.Data.Binary], - params: () => ({ - file: PD.File(), - label: PD.Text('', { isOptional: true }), - isBinary: PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)', isOptional: true }) - }), - apply({ params: p }, globalCtx: PluginContext) { + apply({ params: p }) { return Task.create('Open File', async ctx => { const data = await readFromFile(p.file, p.isBinary ? 'binary' : 'string').runInContext(ctx); return p.isBinary @@ -80,15 +82,16 @@ const ReadFile = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B }); export { ParseCif } -namespace ParseCif { export interface Params { } } -const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({ +type ParseCif = typeof ParseCif +const ParseCif = PluginStateTransform.BuiltIn({ name: 'parse-cif', + from: [SO.Data.String, SO.Data.Binary], + to: SO.Data.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 }) { return Task.create('Parse CIF', async ctx => { const parsed = await (SO.Data.String.is(a) ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx); diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index bdbe5631533fd4915ad7bc080f43d7d329030211..86e0cce2a30a528d0b06d8e6361cb388796a216a 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -23,7 +23,8 @@ namespace StateObject { } export type Type<Cls extends string = string> = { name: string, typeClass: Cls } - export type Ctor = { new(...args: any[]): StateObject, type: any } + export type Ctor<T extends StateObject = StateObject> = { new(...args: any[]): T, type: any } + export type From<C extends Ctor> = C extends Ctor<infer T> ? T : never export function create<Data, T extends Type>(type: T) { return class implements StateObject<Data, T> { diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index a8b95195ce76266fbe45f2c180d2820ed612ef57..6ee6b8b12f4434621f06d82f122891a897bde7c3 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -56,10 +56,7 @@ export namespace Transformer { /** Specify default control descriptors for the parameters */ // export type ParamsDefinition<A extends StateObject = StateObject, P = any> = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any } - export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { - readonly name: string, - readonly from: StateObject.Ctor[], - readonly to: StateObject.Ctor[], + export interface DefinitionBase<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { readonly display?: { readonly name: string, readonly description?: string }, /** @@ -78,8 +75,6 @@ export namespace Transformer { /** Determine if the transformer can be applied automatically on UI change. Default is false. */ canAutoUpdate?(params: AutoUpdateParams<A, B, P>, globalCtx: unknown): boolean, - params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }, - /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean, @@ -90,6 +85,13 @@ export namespace Transformer { readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } + export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> extends DefinitionBase<A, B, P> { + readonly name: string, + readonly from: StateObject.Ctor[], + readonly to: StateObject.Ctor[], + params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }, + } + const registry = new Map<Id, Transformer<any, any>>(); const fromTypeIndex: Map<StateObject.Type, Transformer[]> = new Map(); @@ -140,6 +142,64 @@ export namespace Transformer { return <A extends StateObject, B extends StateObject, P extends {} = {}>(definition: Definition<A, B, P>) => create(namespace, definition); } + export function factory1(namespace: string) { + return Builder.build(namespace); + } + + export namespace Builder { + type ParamDefinition<P> = { [K in keyof P]-?: PD.Base<P[K]> } + + export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor> { + name: string, + from: A | A[], + to: B | B[] + } + + export interface TypeAndParams<A extends StateObject.Ctor, B extends StateObject.Ctor, P> extends Type<A, B> { + params: ParamDefinition<P> + } + + export interface TypeAndParamProvider<A extends StateObject.Ctor, B extends StateObject.Ctor, P> extends Type<A, B> { + paramProvider(a: A, globalCtx: unknown): ParamDefinition<P> + } + + export interface Root { + <A extends StateObject.Ctor, B extends StateObject.Ctor>(info: Type<A, B>): Define<StateObject.From<A>, StateObject.From<B>, {}>, + <A extends StateObject.Ctor, B extends StateObject.Ctor, P>(info: TypeAndParams<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, Params<P>>, + <A extends StateObject.Ctor, B extends StateObject.Ctor, P>(info: TypeAndParamProvider<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, Params<P>> + } + + type Optionals<P> = { [K in keyof P]-?: undefined extends P[K] ? K : never }[keyof P] + type NonOptionals<P> = { [K in keyof P]-?: undefined extends P[K] ? never: K }[keyof P] + type Params<P> = Pick<P, NonOptionals<P>> & Partial<Pick<P, Optionals<P>>> + + export interface Define<A extends StateObject, B extends StateObject, P> { + (def: DefinitionBase<A, B, P>): Transformer<A, B, P> + } + + function root(namespace: string, info: Type<any, any> & TypeAndParams<any, any, any> & TypeAndParamProvider<any, any, any>): Define<any, any, any> { + return def => create(namespace, { + name: info.name, + from: info.from instanceof Array ? info.from : [info.from], + to: info.to instanceof Array ? info.to : [info.to], + params: info.paramProvider + ? info.paramProvider as any + : info.params + ? () => info.params + : void 0, + ...def + }); + } + + export function build(namespace: string): Root { + return (info: any) => root(namespace, info); + } + } + + export function build(namespace: string): Builder.Root { + return Builder.build(namespace); + } + export const ROOT = create<any, any, {}>('build-in', { name: 'root', from: [], diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts index 0ce875ef16c8aa4b52962fb35f9f48b1a519a1fc..fcca72f35476165fe593bcd5e2f6c75623a81ade 100644 --- a/src/mol-state/tree/builder.ts +++ b/src/mol-state/tree/builder.ts @@ -59,9 +59,9 @@ namespace StateTreeBuilder { return new To(this.state, t.ref, this.root); } - update<T extends Transformer<A, any, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root + update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root update(params: any): Root - update<T extends Transformer<A, any, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { + update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { let params: any; if (provider) { const old = this.state.tree.transforms.get(this.ref)!; diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a31e1e2c825f86ad0cb37a44397dce879eb6b4f1..9249d76c536d62b3fb7896d3126506b27c1217da 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -14,8 +14,8 @@ export namespace ParamDefinition { export interface Info { label?: string, description?: string, + isHidden?: boolean, isOptional?: boolean, - isHidden?: boolean } function setInfo<T extends Info>(param: T, info?: Info): T { @@ -31,6 +31,11 @@ export namespace ParamDefinition { defaultValue: T } + export function makeOptional<T>(p: Base<T>): Base<T | undefined> { + p.isOptional = true; + return p; + } + export interface Value<T> extends Base<T> { type: 'value' }