diff --git a/src/apps/state-docs/pd-to-md.ts b/src/apps/state-docs/pd-to-md.ts index b285668073b8b80f070ff20d1b201f45fcda0943..310962f40f1f96d053db19466506e87595dfc8da 100644 --- a/src/apps/state-docs/pd-to-md.ts +++ b/src/apps/state-docs/pd-to-md.ts @@ -28,6 +28,8 @@ function paramInfo(param: PD.Any, offset: number): string { case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`; case 'mapped': return `Object { name: string, params: object } where name+params are:\n${getMapped(param, offset + 2)}`; case 'line-graph': return `A list of 2d vectors [xi, yi][]`; + // TODO: support more languages + case 'script-expression': return `An expression in the specified language { language: 'mol-script', expressiong: string }`; default: const _: never = param; console.warn(`${_} has no associated UI component`); diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 3fd3b6d97a0e6bf0a5419f01dcf0959a5cf0d628..95d5b9673a3fc14fd36ab38b69714d0f9d6e708a 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -40,6 +40,7 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel), PluginSpec.Action(StateTransforms.Model.StructureFromModel), PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory), + PluginSpec.Action(StateTransforms.Model.UserStructureSelection), PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4), PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D), PluginSpec.Action(StateTransforms.Representation.StructureLabels3D), diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index fd46f04fad369b6cb8eee92d9eb65d9b268f0069..dd52f03e21207603c8123c540b2ef209939be7f2 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -22,6 +22,8 @@ import { stringToWords } from 'mol-util/string'; import { PluginStateObject as SO, PluginStateTransform } from '../objects'; import { trajectoryFromGRO } from 'mol-model-formats/structure/gro'; import { parseGRO } from 'mol-io/reader/gro/parser'; +import { parseMolScript } from 'mol-script/language/parser'; +import { transpileMolScript } from 'mol-script/script/mol-script/symbols'; export { TrajectoryFromMmCif }; export { TrajectoryFromPDB }; @@ -31,8 +33,10 @@ export { StructureFromModel }; export { StructureAssemblyFromModel }; export { StructureSymmetryFromModel }; export { StructureSelection }; +export { UserStructureSelection }; export { StructureComplexElement }; export { CustomModelProperties }; + type TrajectoryFromMmCif = typeof TrajectoryFromMmCif const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({ name: 'trajectory-from-mmcif', @@ -243,6 +247,31 @@ const StructureSelection = PluginStateTransform.BuiltIn({ } }); +type UserStructureSelection = typeof UserStructureSelection +const UserStructureSelection = PluginStateTransform.BuiltIn({ + name: 'user-structure-selection', + display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified query expression.' }, + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: { + query: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :residue-test (= atom.resname ALA))' }), + label: PD.makeOptional(PD.Text('')) + } +})({ + apply({ a, params }) { + // TODO: use cache, add "update" + const parsed = parseMolScript(params.query.expression); + if (parsed.length === 0) throw new Error('No query'); + const query = transpileMolScript(parsed[0]); + const compiled = compile<Sel>(query); + const result = compiled(new QueryContext(a.data)); + const s = Sel.unionStructure(result); + if (s.elementCount === 0) return StateObject.Null; + const props = { label: `${params.label || 'Selection'}`, description: structureDesc(s) }; + return new SO.Molecule.Structure(s, props); + } +}); + namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index ed0baaa35f518ad4dcf46f6b0c8f4b02d84ea619..c09203ccdf7e27ca81bbe27594b0ac10518e95cb 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -63,6 +63,7 @@ function controlFor(param: PD.Any): ParamControl | undefined { case 'group': return GroupControl; case 'mapped': return MappedControl; case 'line-graph': return LineGraphControl; + case 'script-expression': return ScriptExpressionControl; default: const _: never = param; console.warn(`${_} has no associated UI component`); @@ -77,7 +78,7 @@ export interface ParamProps<P extends PD.Base<any> = PD.Base<any>> { name: strin export type ParamControl = React.ComponentClass<ParamProps<any>> export abstract class SimpleParam<P extends PD.Any> extends React.PureComponent<ParamProps<P>> { - protected update(value: any) { + protected update(value: P['defaultValue']) { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } @@ -562,3 +563,32 @@ export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converte return <Converted param={this.props.param.converted} value={value} name={this.props.name} onChange={this.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> } } + +export class ScriptExpressionControl extends SimpleParam<PD.ScriptExpression> { + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const value = e.target.value; + if (value !== this.props.value.expression) { + this.update({ language: this.props.value.language, expression: value }); + } + } + + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { + if (!this.props.onEnter) return; + if ((e.keyCode === 13 || e.charCode === 13)) { + this.props.onEnter(); + } + } + + renderControl() { + // TODO: improve! + + const placeholder = this.props.param.label || camelCaseToWords(this.props.name); + return <input type='text' + value={this.props.value.expression || ''} + placeholder={placeholder} + onChange={this.onChange} + onKeyPress={this.props.onEnter ? this.onKeyPress : void 0} + disabled={this.props.isDisabled} + />; + } +} \ No newline at end of file diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index 46d8abbfeb457cfbb5ae83f320374cb77cd266e2..3bb4e3ed84140937872cf12da198e9c63ed3501f 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -213,7 +213,14 @@ export namespace ParamDefinition { return { type: 'conditioned', select: Select<string>(conditionForValue(defaultValue) as string, options), defaultValue, conditionParams, conditionForValue, conditionedValue }; } - export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph | ColorScale<any> | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> + export interface ScriptExpression extends Base<{ language: 'mol-script', expression: string }> { + type: 'script-expression' + } + export function ScriptExpression(defaultValue: ScriptExpression['defaultValue'], info?: Info): ScriptExpression { + return setInfo<ScriptExpression>({ type: 'script-expression', defaultValue }, info) + } + + export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph | ColorScale<any> | Group<any> | Mapped<any> | Converted<any, any> | Conditioned<any, any, any> | ScriptExpression export type Params = { [k: string]: Any } export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] } @@ -301,6 +308,9 @@ export namespace ParamDefinition { return true; } else if (p.type === 'vec3') { return Vec3Data.equals(a, b); + } else if (p.type === 'script-expression') { + const u = a as ScriptExpression['defaultValue'], v = b as ScriptExpression['defaultValue']; + return u.language === v.language && u.expression === v.expression; } else if (typeof a === 'object' && typeof b === 'object') { return shallowEqual(a, b); }