Selaa lähdekoodia

mol-plugin: custom controls for volume streaming

David Sehnal 5 vuotta sitten
vanhempi
commit
5bd3ef0308

+ 1 - 0
src/apps/viewer/index.ts

@@ -24,6 +24,7 @@ function init() {
         actions: [...DefaultPluginSpec.actions, PluginSpec.Action(CreateJoleculeState)],
         behaviors: [...DefaultPluginSpec.behaviors],
         animations: [...DefaultPluginSpec.animations || []],
+        customParamEditors: DefaultPluginSpec.customParamEditors,
         layout: {
             initial: {
                 isExpanded: true,

+ 6 - 4
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -76,15 +76,18 @@ export namespace VolumeStreaming {
         };
     }
 
-    type RT = typeof createParams extends (...args: any[]) => (infer T) ? T : never
-    export type Params = RT extends PD.Params ? PD.Values<RT> : {}
+    export type ParamDefinition = typeof createParams extends (...args: any[]) => (infer T) ? T : never
+    export type Params = ParamDefinition extends PD.Params ? PD.Values<ParamDefinition> : {}
+
+    type CT = typeof channelParam extends (...args: any[]) => (infer T) ? T : never
+    export type ChannelParams = CT extends PD.Group<infer T> ? T : {}
 
     type ChannelsInfo = { [name in ChannelType]?: { isoValue: VolumeIsoValue, color: Color, wireframe: boolean, opacity: number } }
     type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData }
 
     export type ChannelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)'
     export const ChannelTypeOptions: [ChannelType, string][] = [['em', 'em'], ['2fo-fc', '2fo-fc'], ['fo-fc(+ve)', 'fo-fc(+ve)'], ['fo-fc(-ve)', 'fo-fc(-ve)']]
-    interface ChannelInfo {
+    export interface ChannelInfo {
         data: VolumeData,
         color: Color,
         wireframe: boolean,
@@ -96,7 +99,6 @@ export namespace VolumeStreaming {
     export class Behavior extends PluginBehavior.WithSubscribers<Params> {
         private cache = LRUCache.create<ChannelsData>(25);
         public params: Params = {} as any;
-        // private ref: string = '';
 
         channels: Channels = {}
 

+ 4 - 0
src/mol-plugin/index.ts

@@ -17,6 +17,7 @@ import { StateActions } from './state/actions';
 import { InitVolumeStreaming, BoxifyVolumeStreaming, CreateVolumeStreamingBehavior } from './behavior/dynamic/volume-streaming/transformers';
 import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction';
 import { TransformStructureConformation } from './state/actions/structure';
+import { VolumeStreamingCustomControls } from './ui/custom/volume';
 
 export const DefaultPluginSpec: PluginSpec = {
     actions: [
@@ -68,6 +69,9 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }),
         PluginSpec.Behavior(StructureRepresentationInteraction)
     ],
+    customParamEditors: [
+        [CreateVolumeStreamingBehavior, VolumeStreamingCustomControls]
+    ],
     animations: [
         AnimateModelIndex,
         AnimateAssemblyUnwind,

+ 2 - 1
src/mol-plugin/skin/base/components/controls.scss

@@ -253,12 +253,13 @@
     top: 0;
     width: $control-label-width + $control-spacing;
     text-align: left;
+    background: transparent;
     
     .msp-icon {
         line-height: $row-height - 3;
         width: $row-height - 1;
         text-align: center;
-        display: inline-block;
+        // display: inline-block;
         font-size: 100%;
     }
 }

+ 35 - 1
src/mol-plugin/ui/controls/common.tsx

@@ -5,6 +5,7 @@
  */
 
 import * as React from 'react';
+import { Color } from 'mol-util/color';
 
 export class ControlGroup extends React.Component<{ header: string, initialExpanded?: boolean }, { isExpanded: boolean }> {
     state = { isExpanded: !!this.props.initialExpanded }
@@ -94,15 +95,48 @@ export function IconButton(props: {
     title?: string,
     toggleState?: boolean,
     disabled?: boolean,
+    customClass?: string,
     'data-id'?: string
 }) {
-    let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}`;
+    let className = `msp-btn-link msp-btn-icon${props.isSmall ? '-small' : ''}${props.customClass ? ' ' + props.customClass : ''}`;
     if (typeof props.toggleState !== 'undefined') className += ` msp-btn-link-toggle-${props.toggleState ? 'on' : 'off'}`
     return <button className={className} onClick={props.onClick} title={props.title} disabled={props.disabled} data-id={props['data-id']}>
         <span className={`msp-icon msp-icon-${props.icon}`}/>
     </button>;
 }
 
+export class ExpandableGroup extends React.Component<{
+    label: string,
+    colorStripe?: Color,
+    pivot: JSX.Element,
+    controls: JSX.Element
+}, { isExpanded: boolean }> {
+    state = { isExpanded: false };
+
+    toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded });
+
+    render() {
+        const { label, pivot, controls } = this.props;
+        // TODO: fix the inline CSS
+        return <>
+            <div className='msp-control-row'>
+                <span>
+                    {label}
+                    <button className='msp-btn-link msp-btn-icon msp-conrol-group-expander' onClick={this.toggleExpanded} title={`${this.state.isExpanded ? 'Less' : 'More'} options`}
+                        style={{ background: 'transparent', textAlign: 'left', padding: '0' }}>
+                        <span className={`msp-icon msp-icon-${this.state.isExpanded ? 'minus' : 'plus'}`} style={{ display: 'inline-block' }} />
+                    </button>
+                </span>
+                <div>{pivot}</div>
+                {this.props.colorStripe && <div className='msp-expandable-group-color-stripe' style={{ backgroundColor: Color.toStyle(this.props.colorStripe) }} /> }
+            </div>
+            {this.state.isExpanded && <div className='msp-control-offset'>
+                {controls}
+            </div>}
+        </>;
+    }
+}
+
 
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,

+ 189 - 0
src/mol-plugin/ui/custom/volume.tsx

@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginUIComponent } from '../base';
+import { StateTransformParameters } from '../state/common';
+import * as React from 'react';
+import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume-streaming/behavior';
+import { ExpandableGroup } from '../controls/common';
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+import { ParameterControls, ParamOnChange } from '../controls/parameters';
+import { Slider } from '../controls/slider';
+import { VolumeIsoValue, VolumeData } from 'mol-model/volume';
+import { Vec3 } from 'mol-math/linear-algebra';
+
+const ChannelParams = {
+    color: PD.Color(0 as any),
+    wireframe: PD.Boolean(false),
+    opacity: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 })
+};
+type ChannelParams = PD.Values<typeof ChannelParams>
+
+function Channel(props: {
+    label: string,
+    name: VolumeStreaming.ChannelType,
+    channels: { [k: string]: VolumeStreaming.ChannelParams },
+    isRelative: boolean,
+    params: StateTransformParameters.Props,
+    stats: VolumeData['dataStats'],
+    changeIso: (name: string, value: number, isRelative: boolean) => void
+    changeParams: (name: string, param: string, value: any) => void
+}) {
+    const { isRelative, stats } = props;
+    const channel = props.channels[props.name]!;
+
+    const { min, max, mean, sigma } = stats;
+    const value = channel.isoValue.kind === 'relative' ? channel.isoValue.relativeValue : channel.isoValue.absoluteValue;
+    const relMin = (min - mean) / sigma;
+    const relMax = (max - mean) / sigma;
+
+    return <ExpandableGroup
+        label={props.label + (props.isRelative ? ' \u03C3' : '')}
+        colorStripe={channel.color}
+        pivot={<Slider value={value} min={isRelative ? relMin : min} max={isRelative ? relMax : max}
+            step={isRelative ? sigma / 100 : Math.round(((max - min) / sigma)) / 100}
+            onChange={v => props.changeIso(props.name, v, isRelative)} disabled={props.params.isDisabled} onEnter={props.params.events.onEnter} />}
+        controls={<ParameterControls onChange={({ name, value }) => props.changeParams(props.name, name, value)} params={ChannelParams} values={channel} onEnter={props.params.events.onEnter} />}
+    />;
+}
+
+export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransformParameters.Props> {
+
+    private areInitial(params: any) {
+        return PD.areEqual(this.props.info.params, params, this.props.info.initialValues);
+    }
+
+    private newParams(params: VolumeStreaming.Params) {
+        this.props.events.onChange(params, this.areInitial(params));
+    }
+
+    changeIso = (name: string, value: number, isRelative: boolean) => {
+        const old = this.props.params;
+        this.newParams({
+            ...old,
+            channels: {
+                ...old.channels,
+                [name]: {
+                    ...old.channels[name],
+                    isoValue: isRelative ? VolumeIsoValue.relative(value) : VolumeIsoValue.absolute(value)
+                }
+            }
+        });
+    };
+
+    changeParams = (name: string, param: string, value: any) => {
+        const old = this.props.params;
+        this.newParams({
+            ...old,
+            channels: {
+                ...old.channels,
+                [name]: {
+                    ...old.channels[name],
+                    [param]: value
+                }
+            }
+        });
+    };
+
+    convert(channel: any, stats: VolumeData['dataStats'], isRelative: boolean) {
+        return { ...channel, isoValue: isRelative
+            ? VolumeIsoValue.toRelative(channel.isoValue, stats)
+            : VolumeIsoValue.toAbsolute(channel.isoValue, stats) }
+    }
+
+    changeOption: ParamOnChange = ({ value }) => {
+        const b = (this.props.b as VolumeStreaming).data;
+        const isEM = b.info.kind === 'em';
+
+        const isRelative = value.params.isRelative;
+        const sampling = b.info.header.sampling[0];
+        const old = this.props.params as VolumeStreaming.Params, oldChannels = old.channels as any;
+
+        const oldView = old.view.name === value.name
+            ? old.view.params
+            : (this.props.info.params as VolumeStreaming.ParamDefinition).view.map(value.name).defaultValue;
+
+        const viewParams = { ...oldView };
+        if (value.name === 'selection-box') {
+            viewParams.radius = value.params.radius;
+        } else if (value.name === 'box') {
+            viewParams.bottomLeft = value.params.bottomLeft;
+            viewParams.topRight = value.params.topRight;
+        }
+
+        this.newParams({
+            ...old,
+            view: {
+                name: value.name,
+                params: viewParams
+            },
+            detailLevel: value.params.detailLevel,
+            channels: isEM
+                ? { em: this.convert(oldChannels.em, sampling.valuesInfo[0], isRelative) }
+                : {
+                    '2fo-fc': this.convert(oldChannels['2fo-fc'], sampling.valuesInfo[0], isRelative),
+                    'fo-fc(+ve)': this.convert(oldChannels['fo-fc(+ve)'], sampling.valuesInfo[1], isRelative),
+                    'fo-fc(-ve)': this.convert(oldChannels['fo-fc(-ve)'], sampling.valuesInfo[1], isRelative)
+                }
+        });
+    };
+
+    render() {
+        if (!this.props.b) return null;
+
+        const b = (this.props.b as VolumeStreaming).data;
+        const isEM = b.info.kind === 'em';
+        const pivot = isEM ? 'em' : '2fo-fc';
+
+        const params = this.props.params as VolumeStreaming.Params;
+        const isRelative = ((params.channels as any)[pivot].isoValue as VolumeIsoValue).kind === 'relative';
+
+        const sampling = b.info.header.sampling[0];
+
+        // TODO: factor common things out
+        const OptionsParams = {
+            view: PD.MappedStatic(params.view.name, {
+                'box': PD.Group({
+                    bottomLeft: PD.Vec3(Vec3.zero()),
+                    topRight: PD.Vec3(Vec3.zero()),
+                    detailLevel: this.props.info.params.detailLevel,
+                    isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
+                }, { description: 'Static box defined by cartesian coords.' }),
+                'selection-box': PD.Group({
+                    radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }),
+                    detailLevel: this.props.info.params.detailLevel,
+                    isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
+                }, { description: 'Box around last-interacted element.' }),
+                'cell': PD.Group({
+                    detailLevel: this.props.info.params.detailLevel,
+                    isRelative: PD.Boolean(isRelative, { description: 'Use relative or absolute iso values.' })
+                }, { description: 'Box around the structure\'s bounding box.' }),
+                // 'auto': PD.Group({  }), // based on camera distance/active selection/whatever, show whole structure or slice.
+            }, { options: [['box', 'Bounded Box'], ['selection-box', 'Selection'], ['cell', 'Whole Structure']] })
+        };
+        const options = {
+            view: {
+                name: params.view.name,
+                params: {
+                    detailLevel: params.detailLevel,
+                    radius: (params.view.params as any).radius,
+                    bottomLeft: (params.view.params as any).bottomLeft,
+                    topRight: (params.view.params as any).topRight,
+                    isRelative
+                }
+            }
+        };
+
+        return <>
+            {!isEM && <Channel label='2Fo-Fc' name='2fo-fc' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
+            {!isEM && <Channel label='Fo-Fc(+ve)' name='fo-fc(+ve)' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
+            {!isEM && <Channel label='Fo-Fc(-ve)' name='fo-fc(-ve)' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[1]} />}
+            {isEM && <Channel label='EM' name='em' channels={params.channels} changeIso={this.changeIso} changeParams={this.changeParams} isRelative={isRelative} params={this.props} stats={sampling.valuesInfo[0]} />}
+
+            <ParameterControls onChange={this.changeOption} params={OptionsParams} values={options} onEnter={this.props.events.onEnter} />
+        </>
+    }
+}

+ 1 - 0
src/mol-plugin/ui/state/apply-action.tsx

@@ -47,6 +47,7 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App
     canAutoApply() { return false; }
     applyText() { return 'Apply'; }
     isUpdate() { return false; }
+    getSourceAndTarget() { return { a: this.props.state.cells.get(this.props.nodeRef)!.obj }; }
 
     private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef));
 

+ 7 - 3
src/mol-plugin/ui/state/common.tsx

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { State, StateTransform, StateTransformer, StateAction } from 'mol-state';
+import { State, StateTransform, StateTransformer, StateAction, StateObject } from 'mol-state';
 import * as React from 'react';
 import { PurePluginUIComponent } from '../base';
 import { ParameterControls, ParamOnChange } from '../controls/parameters';
@@ -48,7 +48,9 @@ namespace StateTransformParameters {
             onEnter: () => void,
         }
         params: any,
-        isDisabled?: boolean
+        isDisabled?: boolean,
+        a?: StateObject,
+        b?: StateObject
     }
 
     export type Class = React.ComponentClass<Props>
@@ -106,6 +108,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ComponentSta
     abstract canAutoApply(newParams: any): boolean;
     abstract applyText(): string;
     abstract isUpdate(): boolean;
+    abstract getSourceAndTarget(): { a?: StateObject, b?: StateObject };
     abstract state: S;
 
     private busy: Subject<boolean>;
@@ -185,6 +188,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ComponentSta
         //     : 'msp-transform-update-wrapper-collapsed'
         //     : 'msp-transform-wrapper';
 
+        const { a, b } = this.getSourceAndTarget();
         return <div className={wrapClass}>
             <div className='msp-transform-header'>
                 <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded} title={display.description}>
@@ -193,7 +197,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ComponentSta
                 </button>
             </div>
             {!isEmpty && !this.state.isCollapsed && <>
-                <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
+                <ParamEditor info={info} a={a} b={b} events={this.events} params={this.state.params} isDisabled={this.state.busy} />
 
                 <div className='msp-transform-apply-wrap'>
                     <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'><Icon name='cw' /></button>

+ 6 - 0
src/mol-plugin/ui/state/update-transform.tsx

@@ -35,6 +35,12 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr
     canApply() { return !this.state.error && !this.state.busy && !this.state.isInitial; }
     applyText() { return this.canApply() ? 'Update' : 'Nothing to Update'; }
     isUpdate() { return true; }
+    getSourceAndTarget() {
+        return {
+            a: this.props.state.cells.get(this.props.transform.parent)!.obj,
+            b: this.props.state.cells.has(this.props.transform.ref)! ? this.props.state.cells.get(this.props.transform.ref)!.obj : void 0
+        };
+    }
 
     canAutoApply(newParams: any) {
         const autoUpdate = this.props.transform.transformer.definition.canAutoUpdate