diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index c5e7d6846476c96debb79f3c71ad37e93ff9acd8..1f51130de16838fac9d508b56e5f394bcba8fcdf 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -10,12 +10,13 @@ import { StateTransforms } from './state/transforms'; 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'; export class PluginContext { private disposed = false; private ev = RxEventHelper.create(); - readonly state = new PluginState(); + readonly state = new PluginState(this); readonly events = { stateUpdated: this.ev<undefined>() @@ -54,11 +55,25 @@ export class PluginContext { _test_createState(url: string) { const b = StateTree.build(this.state.data.tree); + + const query = MolScriptBuilder.struct.generator.atomGroups({ + // 'atom-test': MolScriptBuilder.core.rel.eq([ + // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), + // MolScriptBuilder.es('C') + // ]), + 'residue-test': MolScriptBuilder.core.rel.eq([ + MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), + 'ALA' + ]) + }); + const newTree = b.toRoot() .apply(StateTransforms.Data.Download, { url }) .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Model.CreateModelsFromMmCif, {}, { ref: 'models' }) + .apply(StateTransforms.Model.ParseModelsFromMmCif, {}, { ref: 'models' }) .apply(StateTransforms.Model.CreateStructureFromModel, { modelIndex: 0 }, { ref: 'structure' }) + .apply(StateTransforms.Model.CreateStructureAssembly) + .apply(StateTransforms.Model.CreateStructureSelection, { query, label: 'ALA residues' }) .apply(StateTransforms.Visuals.CreateStructureRepresentation) .getTree(); @@ -81,7 +96,7 @@ export class PluginContext { this.state.data.context.events.object.updated.subscribe(o => { const oo = o.obj; if (!SO.StructureRepresentation3D.is(oo)) return; - console.log('adding repr', oo.data.repr); + console.log('updating repr', oo.data.repr); this.canvas3d.add(oo.data.repr); this.canvas3d.requestDraw(true); }); diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index b45f9b86196c1988441925e8e3db878d92bcc9c7..2d6b40f2045b53c9edb207b7afe2e116b993c086 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -10,7 +10,7 @@ import { PluginStateObjects as SO } from './state/objects'; export { PluginState } class PluginState { - readonly data = State.create(new SO.Root({ label: 'Root' }, { })); + readonly data: State; getSnapshot(): PluginState.Snapshot { throw 'nyi'; @@ -27,6 +27,10 @@ class PluginState { dispose() { this.data.dispose(); } + + constructor(globalContext: unknown) { + this.data = State.create(new SO.Root({ label: 'Root' }, { }), { globalContext }); + } } namespace PluginState { diff --git a/src/mol-plugin/state/base.ts b/src/mol-plugin/state/base.ts index 710e8079bc2f771087c5a1f73063c69239305231..0c1a22111ce70c20c97f0540fa05ef0ee38a44fc 100644 --- a/src/mol-plugin/state/base.ts +++ b/src/mol-plugin/state/base.ts @@ -11,11 +11,11 @@ export type TypeClass = 'root' | 'data' | 'prop' export namespace PluginStateObject { export type TypeClass = 'Root' | 'Group' | 'Data' | 'Object' | 'Representation' | 'Behaviour' export interface TypeInfo { name: string, shortName: string, description: string, typeClass: TypeClass } - export interface Props { label: string, desctiption?: string } + export interface Props { label: string, description?: string } export const Create = StateObject.factory<TypeInfo, Props>(); } export namespace PluginStateTransform { export const Create = Transformer.factory('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 6ac2fa93558a919cfc01298461a4c7476fe25fd5..7d4aaf81d4af067d8fa1891684f0071af5c903c7 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -9,6 +9,7 @@ import { PluginStateObjects as SO } from '../objects'; import { Task } from 'mol-task'; import CIF from 'mol-io/reader/cif' import { PluginContext } from 'mol-plugin/context'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; export { Download } namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } } @@ -20,6 +21,12 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B }, from: [SO.Root], to: [SO.Data.String, SO.Data.Binary], + params: { + controls: () => ({ + url: PD.Text('URL', 'Resource URL. Must be the same domain or support CORS.', ''), + isBinary: PD.Boolean('Binary', 'If true, download data as binary (string otherwise)', false) + }) + }, apply({ params: p }, globalCtx: PluginContext) { return Task.create('Download', async ctx => { // TODO: track progress diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index e75e0455b9ed4e2f2bbf708bf4a97a91fc2562a3..535003639479689f9088f7a919060139f1addffb 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -7,19 +7,31 @@ import { PluginStateTransform } from '../base'; import { PluginStateObjects as SO } from '../objects'; import { Task } from 'mol-task'; -import { Model, Format, Structure } from 'mol-model/structure'; +import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection } from 'mol-model/structure'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import Expression from 'mol-script/language/expression'; +import { compile } from 'mol-script/runtime/query/compiler'; -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', +export { ParseModelsFromMmCif } +namespace ParseModelsFromMmCif { export interface Params { blockHeader?: string } } +const ParseModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models, ParseModelsFromMmCif.Params>({ + name: 'parse-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], - params: { default: a => ({ blockHeader: a.data.blocks[0].header }) }, + params: { + default: a => ({ blockHeader: a.data.blocks[0].header }), + controls(a) { + const { blocks } = a.data; + if (blocks.length === 0) return {}; + return { + blockHeader: PD.Select('Header', 'Header of the block to parse', blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string])) + }; + } + }, apply({ a, params }) { return Task.create('Parse mmCIF', async ctx => { const header = params.blockHeader || a.data.blocks[0].header; @@ -36,18 +48,73 @@ const CreateModelsFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Models export { CreateStructureFromModel } namespace CreateStructureFromModel { export interface Params { modelIndex: number } } const CreateStructureFromModel = PluginStateTransform.Create<SO.Models, SO.Structure, CreateStructureFromModel.Params>({ - name: 'structure-from-model', + name: 'create-structure-from-model', display: { name: 'Structure from Model', description: 'Create a molecular structure from the specified model.' }, from: [SO.Models], to: [SO.Structure], - params: { default: () => ({ modelIndex: 0 }) }, + params: { + default: () => ({ modelIndex: 0 }), + controls: a => ({ modelIndex: PD.Range('Model Index', 'Model Index', 0, 0, Math.max(0, a.data.length - 1), 1) }) + }, apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); - // TODO: make Structure.ofModel async? const s = Structure.ofModel(a.data[params.modelIndex]); - return new SO.Structure({ label: `${a.data[params.modelIndex].label} (model ${s.models[0].modelNum})`, desctiption: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + return new SO.Structure({ label: `Model ${s.models[0].modelNum}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); } -}); \ No newline at end of file +}); + + +export { CreateStructureAssembly } +namespace CreateStructureAssembly { export interface Params { /** if not specified, use the 1st */ id?: string } } +const CreateStructureAssembly = PluginStateTransform.Create<SO.Structure, SO.Structure, CreateStructureAssembly.Params>({ + name: 'create-structure-assembly', + display: { + name: 'Structure Assembly', + description: 'Create a molecular structure assembly.' + }, + from: [SO.Structure], + to: [SO.Structure], + params: { + default: () => ({ id: void 0 }), + controls(a) { + const { model } = a.data; + const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]); + return { id: PD.Select('Asm Id', 'Assembly Id', ids.length ? ids[0][0] : '', ids) }; + } + }, + isApplicable: a => a.data.models.length === 1 && a.data.model.symmetry.assemblies.length > 0, + apply({ a, params }) { + return Task.create('Build Assembly', async ctx => { + let id = params.id; + const model = a.data.model; + if (!id && model.symmetry.assemblies.length) id = model.symmetry.assemblies[0].id; + const asm = ModelSymmetry.findAssembly(a.data.model, id || ''); + if (!asm) throw new Error(`Assembly '${id}' not found`); + + const s = await StructureSymmetry.buildAssembly(a.data, id!).runInContext(ctx); + return new SO.Structure({ label: `Assembly ${id}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + }) + } +}); + +export { CreateStructureSelection } +namespace CreateStructureSelection { export interface Params { query: Expression, label?: string } } +const CreateStructureSelection = PluginStateTransform.Create<SO.Structure, SO.Structure, CreateStructureSelection.Params>({ + name: 'create-structure-selection', + display: { + name: 'Structure Selection', + description: 'Create a molecular structure from the specified model.' + }, + from: [SO.Structure], + to: [SO.Structure], + apply({ a, params }) { + // TODO: use cache, add "update" + const compiled = compile<StructureSelection>(params.query); + const result = compiled(new QueryContext(a.data)); + const s = StructureSelection.unionStructure(result); + return new SO.Structure({ label: `${params.label || 'Selection'}`, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }, s); + } +}); diff --git a/src/mol-plugin/state/transforms/visuals.ts b/src/mol-plugin/state/transforms/visuals.ts index 80e65bd7979bb2efc41c68e72ac43d5d44756dd7..978143e45412d955336322086aaaee9239bcd8c1 100644 --- a/src/mol-plugin/state/transforms/visuals.ts +++ b/src/mol-plugin/state/transforms/visuals.ts @@ -8,7 +8,8 @@ import { Transformer } from 'mol-state'; import { Task } from 'mol-task'; import { PluginStateTransform } from '../base'; import { PluginStateObjects as SO } from '../objects'; -import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon'; +//import { CartoonRepresentation, DefaultCartoonProps } from 'mol-repr/structure/representation/cartoon'; +import { BallAndStickRepresentation, DefaultBallAndStickProps } from 'mol-repr/structure/representation/ball-and-stick'; export { CreateStructureRepresentation } namespace CreateStructureRepresentation { export interface Params { } } @@ -18,8 +19,8 @@ const CreateStructureRepresentation = PluginStateTransform.Create<SO.Structure, to: [SO.StructureRepresentation3D], apply({ a, params }) { return Task.create('Structure Representation', async ctx => { - const repr = CartoonRepresentation(); - await repr.createOrUpdate({ /* TODO add `webgl: WebGLContext` */ }, { ...DefaultCartoonProps }, a.data).runInContext(ctx); + const repr = BallAndStickRepresentation(); // CartoonRepresentation(); + await repr.createOrUpdate({ /* TODO add `webgl: WebGLContext` */ }, DefaultBallAndStickProps, a.data).runInContext(ctx); return new SO.StructureRepresentation3D({ label: 'Cartoon' }, { repr }); }); }, diff --git a/src/mol-plugin/ui/tree.tsx b/src/mol-plugin/ui/tree.tsx index 0a62f2ca25b4b9431c0ec1e253e7db190a99eea0..4b1a03e2f1302da71d3c58b1886eaf809c25b6ef 100644 --- a/src/mol-plugin/ui/tree.tsx +++ b/src/mol-plugin/ui/tree.tsx @@ -7,6 +7,7 @@ import * as React from 'react'; import { PluginContext } from '../context'; import { PluginStateObject } from 'mol-plugin/state/base'; +import { StateObject } from 'mol-state' export class Tree extends React.Component<{ plugin: PluginContext }, { }> { @@ -25,8 +26,14 @@ export class TreeNode extends React.Component<{ plugin: PluginContext, nodeRef: render() { const n = this.props.plugin.state.data.tree.nodes.get(this.props.nodeRef)!; const obj = this.props.plugin.state.data.objects.get(this.props.nodeRef)!; + if (!obj.obj) { + return <div style={{ borderLeft: '1px solid black', paddingLeft: '5px' }}> + {StateObject.StateType[obj.state]} {obj.errorText} + </div>; + } + const props = obj.obj!.props as PluginStateObject.Props; return <div style={{ borderLeft: '1px solid black', paddingLeft: '5px' }}> - {(obj.obj!.props as PluginStateObject.Props).label} + {props.label} {props.description ? <small>{props.description}</small> : void 0} {n.children.size === 0 ? void 0 : <div style={{ marginLeft: '10px' }}>{n.children.map(c => <TreeNode plugin={this.props.plugin} nodeRef={c!} key={c} />)}</div> diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 0a4932e303fde4f9f906774ec960765dd0ef1447..dacc19a795fb49679a552c7e94ee3b53073fc8ca 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -54,7 +54,7 @@ class State { }); } - constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps: unknown }) { + constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) { const tree = this._tree; const root = tree.getValue(tree.rootRef)!; const defaultObjectProps = (params && params.defaultObjectProps) || { } @@ -82,7 +82,7 @@ namespace State { readonly props: { [key: string]: unknown } } - export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps: unknown }) { + export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) { return new State(rootObject, params); } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index ac38a55a87d96b35d595be1fa419be7c5f89aebc..884c2e097844314849d88b843aba1ee9d697ea25 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]?: ((a: A, globalCtx: unknown) => PD.Any) } + export type ControlsFor<A extends StateObject, Props> = { [P in keyof Props]?: PD.Any } export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { a: A,