diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index 39a00f7baec767151e2c421b255b2635964e5b1c..6da2a78161c86dd45d11dfd98e5b2ad775f7bc09 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -43,7 +43,7 @@ namespace PluginBehavior { export function create<P>(params: CreateParams<P>) { // TODO: cache groups etc - return PluginStateTransform.Create<Root, Behavior, P>({ + return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({ name: params.name, display: params.display, from: [Root], diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index d102d1151fe90875636e21d767e7c13ec4323057..19dee261c1fa076a7729e6524cbbe6512af61230 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}` }; @@ -81,13 +81,14 @@ const DownloadStructure = StateAction.create<PluginStateObject.Root, void, Downl } }); -export const OpenStructure = StateAction.create<PluginStateObject.Root, void, { file: File }>({ - from: [PluginStateObject.Root], +export const OpenStructure = StateAction.build({ + from: PluginStateObject.Root, + params: { file: PD.File({ accept: '.cif,.bcif' }) } +})({ display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, - params: () => ({ file: PD.File({ accept: '.cif,.bcif' }) }), apply({ params, state }) { const b = state.build(); const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); @@ -130,8 +131,9 @@ function complexRepresentation(root: StateTreeBuilder.To<PluginStateObject.Molec // TODO: create spheres visual } -export const CreateComplexRepresentation = StateAction.create<PluginStateObject.Molecule.Structure, void, {}>({ - from: [PluginStateObject.Molecule.Structure], +export const CreateComplexRepresentation = StateAction.build({ + from: PluginStateObject.Molecule.Structure +})({ display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' @@ -143,15 +145,15 @@ export const CreateComplexRepresentation = StateAction.create<PluginStateObject. } }); -export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void, { action: 'advance' | 'reset', by?: number }>({ - from: [], +export const UpdateTrajectory = StateAction.build({ + params: () => ({ + action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), + by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) + }) +})({ display: { name: 'Update Trajectory' }, - params: () => ({ - action: PD.Select('advance', [['advance', 'Advance'], ['reset', 'Reset']]), - by: PD.Numeric(1, { min: -1, max: 1, step: 1 }, { isOptional: true }) - }), apply({ params, state }) { const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model) .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index 24347d8c0a1d1856036b4ee43286a3b9aaa5218f..8f90acb581763d535695903fb3e84aea9c0f5623 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -46,8 +46,6 @@ export namespace PluginStateObject { export namespace Data { export class String extends Create<string>({ name: 'String Data', typeClass: 'Data', }) { } export class Binary extends Create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data' }) { } - export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } - export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } // TODO // export class MultipleRaw extends Create<{ @@ -55,6 +53,11 @@ export namespace PluginStateObject { // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { } } + export namespace Format { + export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } + export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } + } + export namespace Molecule { export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { } export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { } @@ -69,5 +72,6 @@ export namespace PluginStateObject { } export namespace PluginStateTransform { - export const Create = Transformer.factory('ms-plugin'); + export const CreateBuiltIn = Transformer.factory('ms-plugin'); + export const BuiltIn = Transformer.builderFactory('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..8ef6c9cbd712fe9e5ede6df0571adbdaea762fa3 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,20 +82,21 @@ 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.Format.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); if (parsed.isError) throw new Error(parsed.message); - return new SO.Data.Cif(parsed.result); + return new SO.Format.Cif(parsed.result); }); } }); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 9f5efdddd610324cd62d8f1bd75c08f9917dd821..0903e9e504981e5e4aef1a89d04846391c2fdf47 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -16,22 +16,22 @@ import { StateObject } from 'mol-state'; import { PluginContext } from 'mol-plugin/context'; export { TrajectoryFromMmCif } -namespace TrajectoryFromMmCif { export interface Params { blockHeader?: string } } -const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule.Trajectory, TrajectoryFromMmCif.Params>({ +type TrajectoryFromMmCif = typeof TrajectoryFromMmCif +const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({ name: 'trajectory-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.Molecule.Trajectory], + from: SO.Format.Cif, + to: SO.Molecule.Trajectory, params(a) { const { blocks } = a.data; - if (blocks.length === 0) return { }; return { - blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }) + blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })) }; }, +})({ + display: { + name: 'Models from mmCIF', + description: 'Identify and create all separate models in the specified CIF data block' + }, isApplicable: a => a.data.blocks.length > 0, apply({ a, params }) { return Task.create('Parse mmCIF', async ctx => { @@ -48,16 +48,17 @@ const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule export { ModelFromTrajectory } const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1; -namespace ModelFromTrajectory { export interface Params { modelIndex: number } } -const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, SO.Molecule.Model, ModelFromTrajectory.Params>({ +type ModelFromTrajectory = typeof ModelFromTrajectory +const ModelFromTrajectory = PluginStateTransform.BuiltIn({ name: 'model-from-trajectory', + from: SO.Molecule.Trajectory, + to: SO.Molecule.Model, + params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }) +})({ display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Molecule.Trajectory], - to: [SO.Molecule.Model], - params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }), isApplicable: a => a.data.length > 0, apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); @@ -68,15 +69,16 @@ const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, }); export { StructureFromModel } -namespace StructureFromModel { export interface Params { } } -const StructureFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureFromModel.Params>({ +type StructureFromModel = typeof StructureFromModel +const StructureFromModel = PluginStateTransform.BuiltIn({ name: 'structure-from-model', + from: SO.Molecule.Model, + to: SO.Molecule.Structure, +})({ display: { name: 'Structure from Model', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Molecule.Model], - to: [SO.Molecule.Structure], apply({ a }) { let s = Structure.ofModel(a.data); const label = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }; @@ -89,19 +91,20 @@ function structureDesc(s: Structure) { } export { StructureAssemblyFromModel } -namespace StructureAssemblyFromModel { export interface Params { /** if not specified, use the 1st */ id?: string } } -const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureAssemblyFromModel.Params>({ +type StructureAssemblyFromModel = typeof StructureAssemblyFromModel +const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ name: 'structure-assembly-from-model', - display: { - name: 'Structure Assembly', - description: 'Create a molecular structure assembly.' - }, - from: [SO.Molecule.Model], - to: [SO.Molecule.Structure], + from: SO.Molecule.Model, + to: SO.Molecule.Structure, params(a) { const model = a.data; const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]); - return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) }; + return { id: PD.makeOptional(PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' })) }; + } +})({ + display: { + name: 'Structure Assembly', + description: 'Create a molecular structure assembly.' }, apply({ a, params }, plugin: PluginContext) { return Task.create('Build Assembly', async ctx => { @@ -126,19 +129,20 @@ const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model }); export { StructureSelection } -namespace StructureSelection { export interface Params { query: Expression, label?: string } } -const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureSelection.Params>({ +type StructureSelection = typeof StructureSelection +const StructureSelection = PluginStateTransform.BuiltIn({ name: 'structure-selection', + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: () => ({ + query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }), + label: PD.makeOptional(PD.Text('', { isHidden: true })) + }) +})({ display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Structure], - params: () => ({ - query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }), - label: PD.Text('', { isOptional: true, isHidden: true }) - }), apply({ a, params }) { // TODO: use cache, add "update" const compiled = compile<Sel>(params.query); @@ -150,16 +154,18 @@ const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO }); export { StructureComplexElement } -namespace StructureComplexElement { export interface Params { type: 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } } -const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureComplexElement.Params>({ +namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } +type StructureComplexElement = typeof StructureComplexElement +const StructureComplexElement = PluginStateTransform.BuiltIn({ name: 'structure-complex-element', + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: () => ({ type: PD.Text<StructureComplexElement.Types>('atomic-sequence', { isHidden: true }) }), +})({ display: { name: 'Complex Element', description: 'Create a molecular structure from the specified model.' }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Structure], - params: () => ({ type: PD.Text('sequence', { isHidden: true }) }), apply({ a, params }) { // TODO: update function. @@ -169,7 +175,7 @@ const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structur case 'water': query = Queries.internal.water(); label = 'Water'; break; case 'atomic-het': query = Queries.internal.atomicHet(); label = 'HET Groups/Ligands'; break; case 'spheres': query = Queries.internal.spheres(); label = 'Coarse Spheres'; break; - default: throw new Error(`${params.type} is a valid complex element.`); + default: throw new Error(`${params.type} is a not valid complex element.`); } const result = query(new QueryContext(a.data)); diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index c0e2dc23b589b9cacb345975cca2e5861786e3bd..2c3e9c4f9f959f634fcf929a8751da710738d232 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -14,36 +14,31 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { createTheme } from 'mol-theme/theme'; export { StructureRepresentation3D } -namespace StructureRepresentation3D { - export interface Params { - type: { name: string, params: any /** TODO is there "common type" */ }, - colorTheme: { name: string, params: any /** TODO is there "common type" */ }, - sizeTheme: { name: string, params: any /** TODO is there "common type" */ }, - } -} -const StructureRepresentation3D = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Representation3D, StructureRepresentation3D.Params>({ +type StructureRepresentation3D = typeof StructureRepresentation3D +const StructureRepresentation3D = PluginStateTransform.BuiltIn({ name: 'structure-representation-3d', - display: { name: '3D Representation' }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Representation3D], + from: SO.Molecule.Structure, + to: SO.Molecule.Representation3D, params: (a, ctx: PluginContext) => ({ - type: PD.Mapped( + type: PD.Mapped<any>( ctx.structureRepresentation.registry.default.name, ctx.structureRepresentation.registry.types, name => PD.Group<any>(ctx.structureRepresentation.registry.get(name).getParams(ctx.structureRepresentation.themeCtx, a.data))), - colorTheme: PD.Mapped( + colorTheme: PD.Mapped<any>( // TODO how to get a default color theme dependent on the repr type? ctx.structureRepresentation.themeCtx.colorThemeRegistry.default.name, ctx.structureRepresentation.themeCtx.colorThemeRegistry.types, name => PD.Group<any>(ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(name).getParams({ structure: a.data })) ), - sizeTheme: PD.Mapped( + sizeTheme: PD.Mapped<any>( // TODO how to get a default size theme dependent on the repr type? ctx.structureRepresentation.themeCtx.sizeThemeRegistry.default.name, ctx.structureRepresentation.themeCtx.sizeThemeRegistry.types, name => PD.Group<any>(ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(name).getParams({ structure: a.data })) - ), - }), + ) + }) +})({ + display: { name: '3D Representation' }, canAutoUpdate({ oldParams, newParams }) { // TODO: allow for small molecules return oldParams.type.name === newParams.type.name; diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index f29ed9fb5eb5d3d431318af510bb3e805bc554d3..4ef23dd07f35c1377e00b4694221faf0749abede 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -82,13 +82,13 @@ export class State extends PluginComponent { render() { const kind = this.plugin.state.behavior.kind.value; - return <> + return <div className='msp-scrollable-container'> <div className='msp-btn-row-group msp-data-beh'> <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button> </div> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> - </> + </div> } } diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index b1e2ee0e0fd565fecd874e3676762928b78d47a4..264a9266ece3ad3f5213ae2ca2a711378e5e2c99 100644 --- a/src/mol-state/action.ts +++ b/src/mol-state/action.ts @@ -38,8 +38,7 @@ namespace StateAction { params: P } - export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> { - readonly from: StateObject.Ctor[], + export interface DefinitionBase<A extends StateObject = StateObject, T = any, P extends {} = {}> { readonly display?: { readonly name: string, readonly description?: string }, /** @@ -47,12 +46,15 @@ namespace StateAction { */ apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, - 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 } + export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> { + readonly from: StateObject.Ctor[], + params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any } + } + export function create<A extends StateObject, T, P extends {} = {}>(definition: Definition<A, T, P>): StateAction<A, T, P> { const action: StateAction<A, T, P> = { create(params) { return { action, params }; }, @@ -74,4 +76,37 @@ namespace StateAction { } }) } + + export namespace Builder { + export interface Type<A extends StateObject.Ctor, P extends { }> { + from?: A | A[], + params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>) + } + + export interface Root { + <A extends StateObject.Ctor, P extends { }>(info: Type<A, P>): Define<StateObject.From<A>, PD.Normalize<P>> + } + + export interface Define<A extends StateObject, P> { + <T>(def: DefinitionBase<A, T, P>): StateAction<A, T, P> + } + + function root(info: Type<any, any>): Define<any, any> { + return def => create({ + from: info.from instanceof Array + ? info.from + : !!info.from ? [info.from] : [], + params: typeof info.params === 'object' + ? () => info.params as any + : !!info.params + ? info.params as any + : void 0, + ...def + }); + } + + export const build: Root = (info: any) => root(info); + } + + export const build = Builder.build; } \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index bdbe5631533fd4915ad7bc080f43d7d329030211..70917c833285e631724179cfbcaae71b7a18cca9 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -23,12 +23,13 @@ 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> { + return class O implements StateObject<Data, T> { static type = type; - static is(obj?: StateObject): obj is StateObject<Data, T> { return !!obj && type === obj.type; } + static is(obj?: StateObject): obj is O { return !!obj && type === obj.type; } id = UUID.create22(); type = type; label: string; diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 8286db855dfeff046f7a10cf315cdc075e336286..e9ed797ff0b91449b4b43d3fbf4303ae70b49f75 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -311,11 +311,12 @@ function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: State function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { const cell = s.cells.get(n.ref); - if (cell && cell.obj === StateObject.Null) return false; if (!cell || cell.version !== n.version || cell.status === 'error') { s.roots.push(n.ref); return false; } + // nothing below a Null object can be an update root + if (cell && cell.obj === StateObject.Null) return false; return true; } @@ -473,13 +474,13 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) { ctx.results.push(update); if (update.action === 'created') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`)); } else if (update.action === 'updated') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); } else if (update.action === 'replaced') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); } } catch (e) { ctx.changed = true; diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index a8b95195ce76266fbe45f2c180d2820ed612ef57..448272e76a94324ec9c08b773263fc7d7f17be62 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,49 @@ export namespace Transformer { return <A extends StateObject, B extends StateObject, P extends {} = {}>(definition: Definition<A, B, P>) => create(namespace, definition); } + export function builderFactory(namespace: string) { + return Builder.build(namespace); + } + + export namespace Builder { + export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }> { + name: string, + from: A | A[], + to: B | B[], + params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>) + } + + export interface Root { + <A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }>(info: Type<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, PD.Normalize<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, 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: typeof info.params === 'object' + ? () => info.params as any + : !!info.params + ? info.params as any + : 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-theme/color.ts b/src/mol-theme/color.ts index 98695fc948c7ccdbde9c197f2d83a57ecbdea1f5..2368ecae72671b1d35a9a7cd9b0bda7da8f61154 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -56,7 +56,7 @@ namespace ColorTheme { readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P> readonly getParams: (ctx: ThemeDataContext) => P } - export const EmptyProvider: Provider<{}> = { label: '', factory:EmptyFactory, getParams: () => ({}) } + export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) } export class Registry { private _list: { name: string, provider: Provider<any> }[] = [] diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a31e1e2c825f86ad0cb37a44397dce879eb6b4f1..77848d8e78ccea62444eb56f929756b53ff0ea4b 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -14,23 +14,27 @@ export namespace ParamDefinition { export interface Info { label?: string, description?: string, - isOptional?: boolean, - isHidden?: boolean + isHidden?: boolean, } function setInfo<T extends Info>(param: T, info?: Info): T { if (!info) return param; if (info.description) param.description = info.description; if (info.label) param.label = info.label; - if (info.isOptional) param.isOptional = info.isOptional; if (info.isHidden) param.isHidden = info.isHidden; return param; } export interface Base<T> extends Info { + isOptional?: boolean, 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' } @@ -63,11 +67,11 @@ export namespace ParamDefinition { return setInfo<Boolean>({ type: 'boolean', defaultValue }, info) } - export interface Text extends Base<string> { + export interface Text<T extends string = string> extends Base<T> { type: 'text' } - export function Text(defaultValue: string = '', info?: Info): Text { - return setInfo<Text>({ type: 'text', defaultValue }, info) + export function Text<T extends string = string>(defaultValue: string = '', info?: Info): Text<T> { + return setInfo<Text<T>>({ type: 'text', defaultValue: defaultValue as any }, info) } export interface Color extends Base<ColorData> { @@ -177,6 +181,11 @@ export namespace ParamDefinition { export type Params = { [k: string]: Any } export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] } + 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] + export type Normalize<P> = Pick<P, NonOptionals<P>> & Partial<Pick<P, Optionals<P>>> + export type For<P> = { [K in keyof P]-?: Base<P[K]> } + export function getDefaultValues<T extends Params>(params: T) { const d: { [k: string]: any } = {} for (const k of Object.keys(params)) { @@ -227,6 +236,23 @@ export namespace ParamDefinition { if (u.name !== v.name) return false; const map = p.map(u.name); return isParamEqual(map, u.params, v.params); + } else if (p.type === 'multi-select') { + const u = a as MultiSelect<any>['defaultValue'], v = b as MultiSelect<any>['defaultValue']; + if (u.length !== v.length) return false; + if (u.length < 10) { + for (let i = 0, _i = u.length; i < _i; i++) { + if (u[i] === v[i]) continue; + if (v.indexOf(u[i]) < 0) return false; + } + } else { + // TODO: should the value of multiselect be a set? + const vSet = new Set(v); + for (let i = 0, _i = u.length; i < _i; i++) { + if (u[i] === v[i]) continue; + if (!vSet.has(u[i])) return false; + } + } + return true; } else if (p.type === 'interval') { return a[0] === b[0] && a[1] === b[1]; } else if (p.type === 'line-graph') {