diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index d67a0e9396a68933718ec84730cd55f7f22bfaab..a40a21b7d0cc9ea2c0e4af763b4485d6eb2cb948 100644 --- a/src/apps/viewer/index.ts +++ b/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, diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 61d55195d57013528575bcecf7eaa41f2731ce41..1af2657b195c1f9590641a3db6571c6147c883fd 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/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 = {} diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 8f04b4277a76976fd9c0645364b0c19834b42708..732ec22067933fdfd5b642677df4c8a2a0c6ff13 100644 --- a/src/mol-plugin/index.ts +++ b/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, diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index bd527f69b3c6fa427c0ebe08921aa5539b5374b2..ef2dd86f57400c52a5db6db29766f3366da7ae8f 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/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%; } } diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index eea9c464af6de2f2299eea037c168a5ef5859843..71addd5cc48f58f839744099c10f339ada9d4b9d 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/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, diff --git a/src/mol-plugin/ui/custom/volume.tsx b/src/mol-plugin/ui/custom/volume.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f03ff2f052d8184d0c81d3c2c303c5d93e73f210 --- /dev/null +++ b/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} /> + </> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx index e4747abcb041de9eb2d1a3ef29cf9ee3e060b68d..c4b1b211681f72940d10cc513e667cd2aeca443d 100644 --- a/src/mol-plugin/ui/state/apply-action.tsx +++ b/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)); diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx index d46edc26580442335f813e9ea340f6cd0fc3fdd1..5bc1a9aa6be5f1dc24cb1cb3d7895f0894ffc8b9 100644 --- a/src/mol-plugin/ui/state/common.tsx +++ b/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> diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx index 600786fc9c5ccfdf007f4487b9437017e98f0bc6..c48d67128df7db5ec1cd1ffdb014486e4c912ef0 100644 --- a/src/mol-plugin/ui/state/update-transform.tsx +++ b/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