Browse Source

decorator transforms

David Sehnal 5 years ago
parent
commit
ae484443d8

+ 6 - 6
src/mol-plugin-state/actions/structure.ts

@@ -234,19 +234,19 @@ const DownloadStructure = StateAction.build({
             const traj = await plugin.builders.structure.parseTrajectory(data, {
                 formats: downloadParams.map((_, i) => ({ id: '' + i, format: 'cif' as 'cif' }))
             });
-            const model = await plugin.builders.structure.createModel(traj, void 0, supportProps);
-            const struct = await plugin.builders.structure.createStructure(model, src.params.structure.type);
+            const { model } = await plugin.builders.structure.createModel(traj, { properties: supportProps });
+            const { structure } = await plugin.builders.structure.createStructure(model, { structure: src.params.structure.type, properties: supportProps });
             if (createRepr) {
-                await plugin.builders.representation.structurePreset(struct.ref, 'auto');
+                await plugin.builders.representation.structurePreset(structure, 'auto');
             }
         } else {
             for (const download of downloadParams) {
                 const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
                 const traj = await plugin.builders.structure.parseTrajectory(data, format);
-                const model = await plugin.builders.structure.createModel(traj, void 0, supportProps);
-                const struct = await plugin.builders.structure.createStructure(model, src.params.structure.type);
+                const { model } = await plugin.builders.structure.createModel(traj, { properties: supportProps });
+                const { structure } = await plugin.builders.structure.createStructure(model, { structure: src.params.structure.type, properties: supportProps });
                 if (createRepr) {
-                    await plugin.builders.representation.structurePreset(struct.ref, 'auto');
+                    await plugin.builders.representation.structurePreset(structure, 'auto');
                 }
             }
         }

+ 32 - 16
src/mol-plugin-state/builder/structure.ts

@@ -18,6 +18,7 @@ export enum StructureBuilderTags {
     Model = 'model',
     ModelProperties = 'model-properties',
     Structure = 'structure',
+    StructureProperties = 'structure-properties',
     Component = 'structure-component'
 }
 
@@ -78,28 +79,43 @@ export class StructureBuilder {
         }
     }
 
-    async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>, supportProps?: boolean) {
+    async createModel(trajectory: StateObjectRef<SO.Molecule.Trajectory>, params?: {
+        model?: StateTransformer.Params<StateTransforms['Model']['ModelFromTrajectory']>,
+        properties?: boolean | StateTransformer.Params<StateTransforms['Model']['CustomModelProperties']>
+    }) {
         const state = this.dataState;
-        if (supportProps) {
-            const model = state.build().to(trajectory)
-                .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 })
-                .apply(StateTransforms.Model.CustomModelProperties, void 0, { tags: [StructureBuilderTags.Model, StructureBuilderTags.ModelProperties] });
-            await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true }));
-            return model.selector;
-        } else {
-            const model = state.build().to(trajectory)
-                .apply(StateTransforms.Model.ModelFromTrajectory, params || { modelIndex: 0 }, { tags: StructureBuilderTags.Model });        
-            await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true }));
-            return model.selector;
-        }
+
+        const model = state.build().to(trajectory)
+            .apply(StateTransforms.Model.ModelFromTrajectory, params?.model || void 0, { tags: StructureBuilderTags.Model });
+
+        const props = !!params?.properties 
+            ? model.apply(StateTransforms.Model.CustomModelProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.ModelProperties, isDecorator: true })
+            : void 0;
+
+        await this.plugin.runTask(this.dataState.updateTree(model, { revertOnError: true }));
+
+        const modelSelector = model.selector, propertiesSelector = props?.selector;
+
+        return { model: propertiesSelector || modelSelector, index: modelSelector, properties: propertiesSelector };
     }
 
-    async createStructure(model: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params) {
+    async createStructure(model: StateObjectRef<SO.Molecule.Model>, params?: {
+        structure?: RootStructureDefinition.Params,
+        properties?: boolean | StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']>
+    }) {
         const state = this.dataState;
         const structure = state.build().to(model)
-            .apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure });        
+            .apply(StateTransforms.Model.StructureFromModel, { type: params?.structure || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure });        
+
+        const props = !!params?.properties 
+            ? structure.apply(StateTransforms.Model.CustomStructureProperties, typeof params?.properties !== 'boolean' ? params?.properties : void 0, { tags: StructureBuilderTags.StructureProperties, isDecorator: true })
+            : void 0;
+
         await this.plugin.runTask(this.dataState.updateTree(structure, { revertOnError: true }));
-        return structure.selector;
+
+        const structureSelector = structure.selector, propertiesSelector = props?.selector;
+
+        return { structure: propertiesSelector || structureSelector, definition: structureSelector, properties: propertiesSelector };
     }
 
     /** returns undefined if the component is empty/null */

+ 3 - 3
src/mol-plugin-state/transforms/model.ts

@@ -689,7 +689,7 @@ const StructureComponent = PluginStateTransform.BuiltIn({
 type CustomModelProperties = typeof CustomModelProperties
 const CustomModelProperties = PluginStateTransform.BuiltIn({
     name: 'custom-model-properties',
-    display: { name: 'Custom Properties' },
+    display: { name: 'Custom Model Properties' },
     from: SO.Molecule.Model,
     to: SO.Molecule.Model,
     params: (a, ctx: PluginContext) => {
@@ -699,7 +699,7 @@ const CustomModelProperties = PluginStateTransform.BuiltIn({
     apply({ a, params }, ctx: PluginContext) {
         return Task.create('Custom Props', async taskCtx => {
             await attachModelProps(a.data, ctx, taskCtx, params);
-            return new SO.Molecule.Model(a.data, { label: 'Model Props' });
+            return a;
         });
     },
     update({ a, oldParams, newParams }, ctx: PluginContext) {
@@ -745,7 +745,7 @@ const CustomStructureProperties = PluginStateTransform.BuiltIn({
     apply({ a, params }, ctx: PluginContext) {
         return Task.create('Custom Props', async taskCtx => {
             await attachStructureProps(a.data, ctx, taskCtx, params);
-            return new SO.Molecule.Structure(a.data, { label: 'Structure Props' });
+            return a;
         });
     },
     update({ a, oldParams, newParams }, ctx: PluginContext) {

+ 23 - 0
src/mol-plugin-ui/controls/common.tsx

@@ -328,4 +328,27 @@ export class ToggleButton extends React.PureComponent<ToggleButtonProps> {
             {this.props.isSelected ? <b>{label}</b> : label}
         </button>;
     }
+}
+
+export class ExpandGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean, noOffset?: boolean }, { isExpanded: boolean }> {
+    state = { isExpanded: !!this.props.initiallyExpanded };
+
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
+    render() {
+        return <>
+            <div className='msp-control-group-header' style={{ marginTop: '1px' }}>
+                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
+                    <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
+                    {this.props.header}
+                </button>
+            </div>
+            {this.state.isExpanded &&
+                (this.props.noOffset
+                    ? this.props.children
+                    : <div className='msp-control-offset'>
+                        {this.props.children}
+                    </div>)}
+        </>;
+    }
 }

+ 1 - 24
src/mol-plugin-ui/controls/parameters.tsx

@@ -14,7 +14,7 @@ import { camelCaseToWords } from '../../mol-util/string';
 import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
-import { NumericInput, IconButton, ControlGroup, ToggleButton } from './common';
+import { NumericInput, IconButton, ControlGroup, ToggleButton, ExpandGroup } from './common';
 import { _Props, _State, PluginUIComponent } from '../base';
 import { legendFor } from './legend';
 import { Legend as LegendData } from '../../mol-util/legend';
@@ -114,29 +114,6 @@ export class ParameterMappingControl<S, T> extends PluginUIComponent<{ mapping:
     }
 }
 
-class ExpandGroup extends React.PureComponent<{ header: string, initiallyExpanded?: boolean, noOffset?: boolean }, { isExpanded: boolean }> {
-    state = { isExpanded: !!this.props.initiallyExpanded };
-
-    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
-
-    render() {
-        return <>
-            <div className='msp-control-group-header' style={{ marginTop: '1px' }}>
-                <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>
-                    <Icon name={this.state.isExpanded ? 'collapse' : 'expand'} />
-                    {this.props.header}
-                </button>
-            </div>
-            {this.state.isExpanded &&
-                (this.props.noOffset
-                    ? this.props.children
-                    : <div className='msp-control-offset'>
-                        {this.props.children}
-                    </div>)}
-        </>;
-    }
-}
-
 type ParamInfo = [string, PD.Any, ParamControl];
 function classifyParams(params: PD.Params) {
     function addParam(k: string, p: PD.Any, group: typeof essentials) {

+ 27 - 8
src/mol-plugin-ui/plugin.tsx

@@ -19,8 +19,9 @@ import { StateTransform } from '../mol-state';
 import { UpdateTransformControl } from './state/update-transform';
 import { SequenceView } from './sequence';
 import { Toasts } from './toast';
-import { SectionHeader } from './controls/common';
+import { SectionHeader, ExpandGroup } from './controls/common';
 import { LeftPanelControls } from './left-panel';
+import { StateTreeSpine } from '../mol-state/tree/spine';
 
 export class Plugin extends React.Component<{ plugin: PluginContext }, {}> {
     region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
@@ -228,16 +229,34 @@ export class CurrentObject extends PluginUIComponent {
 
         if (!showActions) return null;
 
-        return <>
-            {(cell.status === 'ok' || cell.status === 'error') && <>
+        const actions = cell.status === 'ok' && <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} />
+
+        if (cell.status === 'error') {
+            return <>            
                 <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} />
                 <UpdateTransformControl state={current.state} transform={transform} customHeader='none' />
-            </> }
-            {cell.status === 'ok' &&
-                <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} />
-            }
+                {actions}
+            </>;
+        }
+
+        if (cell.status !== 'ok') return null;
+
+        const decoratorChain = StateTreeSpine.getDecoratorChain(this.current.state, this.current.ref);
+        const parent = decoratorChain[decoratorChain.length - 1];
+
+        let decorators: JSX.Element[] | undefined = decoratorChain.length > 1 ? [] : void 0;
+        for (let i = decoratorChain.length - 2; i >= 0; i--) {
+            const d = decoratorChain[i];
+            decorators!.push(<ExpandGroup header={d.transform.transformer.definition.display.name}>
+                <UpdateTransformControl state={current.state} transform={d.transform} customHeader='none' />
+            </ExpandGroup>);
+        }
 
-            {/* <StateObjectActions state={current.state} nodeRef={ref} initiallyCollapsed />} */}
+        return <>            
+            <SectionHeader icon='flow-cascade' title={`${parent.obj?.label || parent.transform.transformer.definition.display.name}`} desc={parent.transform.transformer.definition.display.name} />
+            <UpdateTransformControl state={current.state} transform={parent.transform} customHeader='none' />
+            {decorators && <div className='msp-controls-section'>{decorators}</div>}
+            {actions}
         </>;
     }
 }

+ 12 - 6
src/mol-plugin-ui/state/tree.tsx

@@ -6,7 +6,7 @@
 
 import * as React from 'react';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
-import { State, StateObject, StateTransform, StateObjectCell } from '../../mol-state'
+import { State, StateTree as _StateTree, StateObject, StateTransform, StateObjectCell } from '../../mol-state'
 import { PluginCommands } from '../../mol-plugin/commands';
 import { PluginUIComponent, _Props, _State } from '../base';
 import { Icon } from '../controls/icons';
@@ -85,6 +85,12 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
         return { isCollapsed: !!props.cell.state.isCollapsed };
     }
 
+    hasDecorator(children: _StateTree.ChildSet) {
+        if (children.size !== 1) return false;
+        const ref = children.values().next().value;
+        return !!this.props.cell.parent.tree.transforms.get(ref).isDecorator;
+    }
+
     render() {
         const cell = this.props.cell;
         if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) {
@@ -92,17 +98,17 @@ class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: nu
         }
 
         const cellState = cell.state;
-        const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || !cell.state.isGhost);
         const children = cell.parent.tree.children.get(this.ref);
-        const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
-
+        const showLabel = (cell.transform.ref !== StateTransform.RootRef) && (cell.status !== 'ok' || (!cell.state.isGhost && !this.hasDecorator(children)));
+        
         if (!showLabel) {
             if (children.size === 0) return null;
             return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
+                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={this.props.depth} />)}
             </div>;
         }
-
+        
+        const newDepth = this.props.depth + 1;
         return <>
             <StateTreeNodeLabel cell={cell} depth={this.props.depth} />
             {children.size === 0

+ 5 - 0
src/mol-state/state/builder.ts

@@ -115,6 +115,11 @@ namespace StateBuilder {
          * If no params are specified (params <- undefined), default params are lazily resolved.
          */
         apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>, T> {
+            if (options?.isDecorator) {
+                const children = this.state.tree.children.get(this.ref);
+                if (children.size > 0) throw new Error('Decorators can only be applied to childless nodes.');
+            }
+
             const t = tr.apply(this.ref, params, options);
             this.state.tree.add(t);
             this.editInfo.count++;

+ 7 - 1
src/mol-state/transform.ts

@@ -14,6 +14,7 @@ interface Transform<T extends StateTransformer = StateTransformer> {
     readonly transformer: T,
     readonly state: Transform.State,
     readonly tags?: string[],
+    readonly isDecorator?: boolean,
     readonly ref: Transform.Ref,
     /**
      * Sibling-like dependency
@@ -90,6 +91,7 @@ namespace Transform {
     export interface Options {
         ref?: string,
         tags?: string | string[],
+        isDecorator?: boolean,
         state?: State,
         dependsOn?: Ref[]
     }
@@ -105,8 +107,9 @@ namespace Transform {
         return {
             parent,
             transformer,
-            state: (options && options.state) || { },
+            state: options?.state || { },
             tags,
+            isDecorator: options?.isDecorator,
             ref,
             dependsOn: options && options.dependsOn,
             params,
@@ -161,6 +164,7 @@ namespace Transform {
         params: any,
         state?: State,
         tags?: string[],
+        isDecorator?: boolean,
         ref: string,
         dependsOn?: string[]
         version: string
@@ -184,6 +188,7 @@ namespace Transform {
             params: t.params ? pToJson(t.params) : void 0,
             state,
             tags: t.tags,
+            isDecorator: t.isDecorator || void 0,
             ref: t.ref,
             dependsOn: t.dependsOn,
             version: t.version
@@ -201,6 +206,7 @@ namespace Transform {
             params: t.params ? pFromJson(t.params) : void 0,
             state: t.state || { },
             tags: t.tags,
+            isDecorator: t.isDecorator,
             ref: t.ref as Ref,
             dependsOn: t.dependsOn,
             version: t.version

+ 10 - 0
src/mol-state/tree/spine.ts

@@ -49,8 +49,18 @@ namespace StateTreeSpine {
         }
 
         constructor(private cells: State.Cells) {
+        }
+    }
 
+    export function getDecoratorChain(state: State, currentRef: StateTransform.Ref): StateObjectCell[] {
+        const cells = state.cells;
+        let current = cells.get(currentRef)!;
+        const ret: StateObjectCell[] = [current];
+        while (current?.transform.isDecorator) {
+            current = cells.get(current.transform.parent)!;
+            ret.push(current);
         }
+        return ret;
     }
 
     export function getRootOfType<T extends StateObject.Ctor>(state: State, t: T, ref: string): StateObject.From<T> | undefined {