diff --git a/CHANGELOG.md b/CHANGELOG.md
index 107e125d13f93b5eb549f7db10ec782ee31cbaa6..e9ff693dc26143f04565512660bfb48933afa824 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file, following t
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
+## [Unreleased]
+
+- Ability to pass ``Canvas3DContext`` to ``PluginContext.fromCanvas``.
+- Relative frame support for ``Canvas3D`` viewport.
+- Fix bug in screenshot copy UI.
+- Add ability to select residues from a list of identifiers to the Selection UI.
+
## [v2.0.4] - 2021-04-20
- [WIP] Mesh export extension
diff --git a/src/mol-plugin-state/manager/structure/selection.ts b/src/mol-plugin-state/manager/structure/selection.ts
index fdb18167586abd1236fbe392e566576597ae8dc1..cc291447b69fa99b6a85d6487db58251321bc7ee 100644
--- a/src/mol-plugin-state/manager/structure/selection.ts
+++ b/src/mol-plugin-state/manager/structure/selection.ts
@@ -10,7 +10,7 @@ import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
+import { QueryContext, Structure, StructureElement, StructureQuery, StructureSelection } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
@@ -457,6 +457,13 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
this.triggerInteraction(modifier, loci, applyGranularity);
}
+ fromCompiledQuery(modifier: StructureSelectionModifier, query: StructureQuery, applyGranularity = true) {
+ for (const s of this.applicableStructures) {
+ const loci = query(new QueryContext(s));
+ this.triggerInteraction(modifier, StructureSelection.toLociWithSourceUnits(loci), applyGranularity);
+ }
+ }
+
fromSelectionQuery(modifier: StructureSelectionModifier, query: StructureSelectionQuery, applyGranularity = true) {
this.plugin.runTask(Task.create('Structure Selection', async runtime => {
for (const s of this.applicableStructures) {
diff --git a/src/mol-plugin-ui/structure/selection.tsx b/src/mol-plugin-ui/structure/selection.tsx
index 9ef8ee808b719567554a20556700f53aea5017f8..151a8240e931b49b519bed6680f3e1bb6a034a11 100644
--- a/src/mol-plugin-ui/structure/selection.tsx
+++ b/src/mol-plugin-ui/structure/selection.tsx
@@ -6,22 +6,24 @@
*/
import * as React from 'react';
-import { StructureSelectionQueries, StructureSelectionQuery, getNonStandardResidueQueries, getElementQueries, getPolymerAndBranchedEntityQueries } from '../../mol-plugin-state/helpers/structure-selection-query';
+import { Structure } from '../../mol-model/structure/structure/structure';
+import { getElementQueries, getNonStandardResidueQueries, getPolymerAndBranchedEntityQueries, StructureSelectionQueries, StructureSelectionQuery } from '../../mol-plugin-state/helpers/structure-selection-query';
import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
import { StructureComponentManager } from '../../mol-plugin-state/manager/structure/component';
-import { StructureRef, StructureComponentRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
+import { StructureComponentRef, StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
import { StructureSelectionModifier } from '../../mol-plugin-state/manager/structure/selection';
+import { PluginContext } from '../../mol-plugin/context';
+import { compileResidueListSelection } from '../../mol-script/util/residue-list';
import { memoizeLatest } from '../../mol-util/memoize';
import { ParamDefinition } from '../../mol-util/param-definition';
-import { stripTags } from '../../mol-util/string';
+import { capitalize, stripTags } from '../../mol-util/string';
import { PluginUIComponent, PurePluginUIComponent } from '../base';
import { ActionMenu } from '../controls/action-menu';
import { Button, ControlGroup, IconButton, ToggleButton } from '../controls/common';
+import { BrushSvg, CancelOutlinedSvg, CloseSvg, CubeOutlineSvg, HelpOutlineSvg, Icon, IntersectSvg, RemoveSvg, RestoreSvg, SelectionModeSvg, SetSvg, SubtractSvg, UnionSvg } from '../controls/icons';
import { ParameterControls, ParamOnChange, PureSelectControl } from '../controls/parameters';
-import { UnionSvg, SubtractSvg, IntersectSvg, SetSvg, CubeOutlineSvg, Icon, SelectionModeSvg, RemoveSvg, RestoreSvg, HelpOutlineSvg, CancelOutlinedSvg, BrushSvg, CloseSvg } from '../controls/icons';
+import { HelpGroup, HelpText, ViewportHelpContent } from '../viewport/help';
import { AddComponentControls } from './components';
-import { Structure } from '../../mol-model/structure/structure/structure';
-import { ViewportHelpContent, HelpGroup, HelpText } from '../viewport/help';
export class ToggleSelectionModeButton extends PurePluginUIComponent<{ inline?: boolean }> {
@@ -47,12 +49,15 @@ const StructureSelectionParams = {
granularity: InteractivityManager.Params.granularity,
};
+type SelectionHelperType = 'residue-list'
+
interface StructureSelectionActionsControlsState {
isEmpty: boolean,
isBusy: boolean,
canUndo: boolean,
- action?: StructureSelectionModifier | 'theme' | 'add-component' | 'help'
+ action?: StructureSelectionModifier | 'theme' | 'add-component' | 'help',
+ helper?: SelectionHelperType,
}
const ActionHeader = new Map<StructureSelectionModifier, string>([
@@ -65,6 +70,7 @@ const ActionHeader = new Map<StructureSelectionModifier, string>([
export class StructureSelectionActionsControls extends PluginUIComponent<{}, StructureSelectionActionsControlsState> {
state = {
action: void 0 as StructureSelectionActionsControlsState['action'],
+ helper: void 0 as StructureSelectionActionsControlsState['helper'],
isEmpty: true,
isBusy: false,
@@ -118,7 +124,16 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
}
}
- get structures () {
+ selectHelper: ActionMenu.OnSelect = (item, e) => {
+ console.log(item);
+ if (!item || !this.state.action) {
+ this.setState({ action: void 0, helper: void 0 });
+ return;
+ }
+ this.setState({ helper: (item.value as { kind: SelectionHelperType }).kind });
+ }
+
+ get structures() {
const structures: Structure[] = [];
for (const s of this.plugin.managers.structure.hierarchy.selection.structures) {
const structure = s.cell.obj?.data;
@@ -129,7 +144,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
private queriesItems: ActionMenu.Items[] = []
private queriesVersion = -1
- get queries () {
+ get queries() {
const { registry } = this.plugin.query.structure;
if (registry.version !== this.queriesVersion) {
const structures = this.structures;
@@ -150,8 +165,25 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
return this.queriesItems;
}
+ private helpersItems?: ActionMenu.Items[] = void 0;
+ get helpers() {
+ if (this.helpersItems) return this.helpersItems;
+ // TODO: this is an initial implementation of the helper UI
+ // the plan is to add support to input queries in different languages
+ // after this has been implemented in mol-script
+ const helpers = [
+ { kind: 'residue-list' as SelectionHelperType, category: 'Helpers', label: 'Residue List', description: 'Create a selection from a list of residue ranges.' }
+ ];
+ this.helpersItems = ActionMenu.createItems(helpers, {
+ label: q => q.label,
+ category: q => q.category,
+ description: q => q.description
+ });
+ return this.helpersItems;
+ }
+
private showAction(q: StructureSelectionActionsControlsState['action']) {
- return () => this.setState({ action: this.state.action === q ? void 0 : q });
+ return () => this.setState({ action: this.state.action === q ? void 0 : q, helper: void 0 });
}
toggleAdd = this.showAction('add')
@@ -187,6 +219,45 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
? `Undo ${this.plugin.state.data.latestUndoLabel}`
: 'Some mistakes of the past can be undone.';
+ let children: React.ReactNode | undefined = void 0;
+
+ if (this.state.action && !this.state.helper) {
+ children = <>
+ {(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-component' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
+ <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
+ <ActionMenu items={this.helpers} onSelect={this.selectHelper} noOffset />
+ </div>}
+ {this.state.action === 'theme' && <div className='msp-selection-viewport-controls-actions'>
+ <ControlGroup header='Theme' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleTheme} topRightIcon={CloseSvg}>
+ <ApplyThemeControls onApply={this.toggleTheme} />
+ </ControlGroup>
+ </div>}
+ {this.state.action === 'add-component' && <div className='msp-selection-viewport-controls-actions'>
+ <ControlGroup header='Add Component' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddComponent} topRightIcon={CloseSvg}>
+ <AddComponentControls onApply={this.toggleAddComponent} forSelection />
+ </ControlGroup>
+ </div>}
+ {this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
+ <ControlGroup header='Help' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelp} topRightIcon={CloseSvg} maxHeight='300px'>
+ <HelpGroup header='Selection Operations'>
+ <HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
+ </HelpGroup>
+ <HelpGroup header='Representation Operations'>
+ <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeOutlineSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create components, remove from components, or undo actions.</HelpText>
+ </HelpGroup>
+ <ViewportHelpContent selectOnly={true} />
+ </ControlGroup>
+ </div>}
+ </>;
+ } else if (ActionHeader.has(this.state.action as any) && this.state.helper === 'residue-list') {
+ const close = () => this.setState({ action: void 0, helper: void 0 });
+ children = <div className='msp-selection-viewport-controls-actions'>
+ <ControlGroup header='Residue List' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={close} topRightIcon={CloseSvg}>
+ <ResidueListSelectionHelper modifier={this.state.action as any} plugin={this.plugin} close={close} />
+ </ControlGroup>
+ </div>;
+ }
+
return <>
<div className='msp-flex-row' style={{ background: 'none' }}>
<PureSelectControl title={`Picking Level for selecting and highlighting`} param={StructureSelectionParams.granularity} name='granularity' value={granularity} onChange={this.setGranuality} isDisabled={this.isDisabled} />
@@ -195,7 +266,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
<ToggleButton icon={IntersectSvg} title={`${ActionHeader.get('intersect')}. Hold shift key to keep menu open.`} toggle={this.toggleIntersect} isSelected={this.state.action === 'intersect'} disabled={this.isDisabled} />
<ToggleButton icon={SetSvg} title={`${ActionHeader.get('set')}. Hold shift key to keep menu open.`} toggle={this.toggleSet} isSelected={this.state.action === 'set'} disabled={this.isDisabled} />
- <ToggleButton icon={BrushSvg} title='Apply Theme to Selection' toggle={this.toggleTheme} isSelected={this.state.action === 'theme'} disabled={this.isDisabled} style={{ marginLeft: '10px' }} />
+ <ToggleButton icon={BrushSvg} title='Apply Theme to Selection' toggle={this.toggleTheme} isSelected={this.state.action === 'theme'} disabled={this.isDisabled} style={{ marginLeft: '10px' }} />
<ToggleButton icon={CubeOutlineSvg} title='Create Component of Selection with Representation' toggle={this.toggleAddComponent} isSelected={this.state.action === 'add-component'} disabled={this.isDisabled} />
<IconButton svg={RemoveSvg} title='Remove/subtract Selection from all Components' onClick={this.subtract} disabled={this.isDisabled} />
<IconButton svg={RestoreSvg} onClick={this.undo} disabled={!this.state.canUndo || this.isDisabled} title={undoTitle} />
@@ -203,30 +274,7 @@ export class StructureSelectionActionsControls extends PluginUIComponent<{}, Str
<ToggleButton icon={HelpOutlineSvg} title='Show/hide help' toggle={this.toggleHelp} style={{ marginLeft: '10px' }} isSelected={this.state.action === 'help'} />
<IconButton svg={CancelOutlinedSvg} title='Turn selection mode off' onClick={this.turnOff} />
</div>
- {(this.state.action && this.state.action !== 'theme' && this.state.action !== 'add-component' && this.state.action !== 'help') && <div className='msp-selection-viewport-controls-actions'>
- <ActionMenu header={ActionHeader.get(this.state.action as StructureSelectionModifier)} title='Click to close.' items={this.queries} onSelect={this.selectQuery} noOffset />
- </div>}
- {this.state.action === 'theme' && <div className='msp-selection-viewport-controls-actions'>
- <ControlGroup header='Theme' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleTheme} topRightIcon={CloseSvg}>
- <ApplyThemeControls onApply={this.toggleTheme} />
- </ControlGroup>
- </div>}
- {this.state.action === 'add-component' && <div className='msp-selection-viewport-controls-actions'>
- <ControlGroup header='Add Component' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleAddComponent} topRightIcon={CloseSvg}>
- <AddComponentControls onApply={this.toggleAddComponent} forSelection />
- </ControlGroup>
- </div>}
- {this.state.action === 'help' && <div className='msp-selection-viewport-controls-actions'>
- <ControlGroup header='Help' title='Click to close.' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleHelp} topRightIcon={CloseSvg} maxHeight='300px'>
- <HelpGroup header='Selection Operations'>
- <HelpText>Use <Icon svg={UnionSvg} inline /> <Icon svg={SubtractSvg} inline /> <Icon svg={IntersectSvg} inline /> <Icon svg={SetSvg} inline /> to modify the selection.</HelpText>
- </HelpGroup>
- <HelpGroup header='Representation Operations'>
- <HelpText>Use <Icon svg={BrushSvg} inline /> <Icon svg={CubeOutlineSvg} inline /> <Icon svg={RemoveSvg} inline /> <Icon svg={RestoreSvg} inline /> to color, create components, remove from components, or undo actions.</HelpText>
- </HelpGroup>
- <ViewportHelpContent selectOnly={true} />
- </ControlGroup>
- </div>}
+ {children}
</>;
}
}
@@ -333,4 +381,34 @@ class ApplyThemeControls extends PurePluginUIComponent<ApplyThemeControlsProps,
</Button>
</>;
}
+}
+
+const ResidueListIdTypeParams = {
+ idType: ParamDefinition.Select<'auth' | 'label'>('auth', ParamDefinition.arrayToOptions(['auth', 'label'])),
+ residues: ParamDefinition.Text('', { description: 'A comma separated list of residue ranges in given chain, e.g. A 10-15, B 25, C 30:i' })
+};
+
+const DefaultResidueListIdTypeParams = ParamDefinition.getDefaultValues(ResidueListIdTypeParams);
+
+function ResidueListSelectionHelper({ modifier, plugin, close }: { modifier: StructureSelectionModifier, plugin: PluginContext, close: () => void }) {
+ const [state, setState] = React.useState(DefaultResidueListIdTypeParams);
+
+ const apply = () => {
+ if (state.residues.length === 0) return;
+
+ try {
+ close();
+ const query = compileResidueListSelection(state.residues, state.idType);
+ plugin.managers.structure.selection.fromCompiledQuery(modifier, query, false);
+ } catch (e) {
+ plugin.log.error(`Failed to create selection: ${e}`);
+ }
+ };
+
+ return <>
+ <ParameterControls params={ResidueListIdTypeParams} values={state} onChangeValues={setState} onEnter={apply} />
+ <Button className='msp-btn-commit msp-btn-commit-on' disabled={state.residues.length === 0} onClick={apply} style={{ marginTop: '1px' }}>
+ {capitalize(modifier)} Selection
+ </Button>
+ </>;
}
\ No newline at end of file
diff --git a/src/mol-script/util/residue-list.ts b/src/mol-script/util/residue-list.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa509e9a7cd8c2475a561b7e94974effaf40b4dc
--- /dev/null
+++ b/src/mol-script/util/residue-list.ts
@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureQuery } from '../../mol-model/structure/query';
+import { Expression } from '../language/expression';
+import { MolScriptBuilder as MS } from '../language/builder';
+import { compile } from '../runtime/query/base';
+
+// TODO: make this into a separate "language"?
+
+type ResidueListSelectionEntry =
+ | { kind: 'single', asym_id: string; seq_id: number; ins_code?: string }
+ | { kind: 'range', asym_id: string; seq_id_beg: number; seq_id_end: number; }
+
+function entriesToQuery(xs: ResidueListSelectionEntry[], kind: 'auth' | 'label') {
+ const groups: Expression[] = [];
+
+ const asym_id_key = kind === 'auth' ? 'auth_asym_id' as const : 'label_asym_id' as const;
+ const seq_id_key = kind === 'auth' ? 'auth_seq_id' as const : 'label_seq_id' as const;
+
+ for (const x of xs) {
+ if (x.kind === 'range') {
+ groups.push(MS.struct.generator.atomGroups({
+ 'chain-test': MS.core.rel.eq([MS.ammp(asym_id_key), x.asym_id]),
+ 'residue-test': MS.core.rel.inRange([MS.ammp(seq_id_key), x.seq_id_beg, x.seq_id_end])
+ }));
+ } else {
+ const ins_code = (x.ins_code ?? '').trim();
+
+ groups.push(MS.struct.generator.atomGroups({
+ 'chain-test': MS.core.rel.eq([MS.ammp(asym_id_key), x.asym_id]),
+ 'residue-test': MS.core.logic.and([
+ MS.core.rel.eq([MS.ammp(seq_id_key), x.seq_id]),
+ MS.core.rel.eq([MS.ammp('pdbx_PDB_ins_code'), ins_code])
+ ])
+ }));
+ }
+ }
+
+ const query = MS.struct.combinator.merge(groups);
+
+ return compile(query) as StructureQuery;
+}
+
+function parseRange(c: string, s: string[], e: number): ResidueListSelectionEntry | undefined {
+ if (!c || s.length === 0 || Number.isNaN(+s[0])) return;
+ if (Number.isNaN(e)) {
+ return { kind: 'single', asym_id: c, seq_id: +s[0], ins_code: s[1] };
+ }
+ return { kind: 'range', asym_id: c, seq_id_beg: +s[0], seq_id_end: e };
+}
+
+function parseInsCode(e?: string) {
+ if (!e) return [];
+ return e.split(':');
+}
+
+function parseResidueListSelection(input: string): ResidueListSelectionEntry[] {
+ return input.split(',') // A 1-3, B 3 => [A 1-3, B 3]
+ .map(e => e.trim().split(/\s+|[-]/g).filter(e => !!e)) // [A 1-3, B 3] => [[A, 1, 3], [B, 3]]
+ .map(e => parseRange(e[0], parseInsCode(e[1]), +e[2]))
+ .filter(e => !!e) as ResidueListSelectionEntry[];
+}
+
+// parses a list of residue ranges, e.g. A 10-100, B 30, C 12:i
+export function compileResidueListSelection(input: string, idType: 'auth' | 'label') {
+ const entries = parseResidueListSelection(input);
+ return entriesToQuery(entries, idType);
+}
\ No newline at end of file