Explorar el Código

wip, structure ui tools

Alexander Rose hace 5 años
padre
commit
e109f069a8

+ 28 - 3
src/mol-plugin/skin/base/components/temp.scss

@@ -48,6 +48,31 @@
     }
 }
 
+.msp-select-row {
+    display:flex;
+    flex-direction:row;
+    height: $row-height;
+    width: inherit;
+
+    > select {
+        margin: 0;
+        flex: 1 1 auto;
+        margin-right: 1px;
+        height: $row-height;
+
+        text-align-last: center;
+        background: none !important;
+
+        > option[value = _] {
+            display: none;
+        }
+    }
+
+    > select:last-child {
+        margin-right: 0;
+    }
+}
+
 .msp-state-list {
     list-style: none;
     margin-top: $control-spacing;
@@ -62,7 +87,7 @@
         > div {
             position: absolute;
             right: 0;
-            top: 0;        
+            top: 0;
         }
     }
 
@@ -88,7 +113,7 @@
 
     &-current {
         // background: $control-background;
-        
+
         a {
             color: $font-color;
         }
@@ -162,7 +187,7 @@
     top: $control-spacing;
     z-index: 10000;
 
-    .msp-traj-controls {        
+    .msp-traj-controls {
         line-height: $row-height;
         float: left;
         margin-right: $control-spacing;

+ 1 - 3
src/mol-plugin/ui/controls.tsx

@@ -16,7 +16,6 @@ import { StateTransforms } from '../../mol-plugin/state/transforms';
 import { StateTransformer } from '../../mol-state';
 import { ModelFromTrajectory } from '../../mol-plugin/state/transforms/model';
 import { AnimationControls } from './state/animation';
-import { StructureOverpaintControls } from './structure/overpaint';
 import { StructureRepresentationControls } from './structure/representation';
 import { StructureSelectionControls } from './structure/selection';
 
@@ -261,8 +260,7 @@ export class StructureToolsWrapper extends PluginUIComponent {
             <div className='msp-section-header'><Icon name='code' /> Structure Tools</div>
 
             <StructureSelectionControls />
-            <StructureOverpaintControls />
             <StructureRepresentationControls />
         </div>;
     }
-}
+}

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

@@ -137,6 +137,25 @@ export class ExpandableGroup extends React.Component<{
     }
 }
 
+export class ButtonSelect extends React.PureComponent<{ label: string, onChange: (value: string) => void }> {
+
+    onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        e.preventDefault()
+        this.props.onChange(e.target.value)
+        e.target.value = '_'
+    }
+
+    render() {
+        return <select value='_' onChange={this.onChange}>
+            <option key='_' value='_'>{this.props.label}</option>
+            {this.props.children}
+        </select>
+    }
+}
+
+export function Options(options: [string, string][]) {
+    return options.map(([value, label]) => <option key={value} value={value}>{label}</option>)
+}
 
 // export const ToggleButton = (props: {
 //     onChange: (v: boolean) => void,

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

@@ -248,7 +248,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> {
 }
 
 let _colors: React.ReactFragment | undefined = void 0;
-function ColorOptions() {
+export function ColorOptions() {
     if (_colors) return _colors;
     _colors = <>{Object.keys(ColorNames).map(name =>
         <option key={name} value={(ColorNames as { [k: string]: Color })[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color })[name])}` }} >

+ 0 - 65
src/mol-plugin/ui/structure/overpaint.tsx

@@ -1,65 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import * as React from 'react';
-import { PluginUIComponent } from '../base';
-import { ParamDefinition as PD} from '../../../mol-util/param-definition';
-import { ColorNames } from '../../../mol-util/color/tables';
-import { ParameterControls } from '../controls/parameters';
-import { PluginContext } from '../../context';
-import { Color } from '../../../mol-util/color';
-
-export class StructureOverpaintControls extends PluginUIComponent<{}, { params: PD.Values<ReturnType<typeof StructureOverpaintControls.getParams>> }> {
-    state = { params: PD.getDefaultValues(StructureOverpaintControls.getParams(this.plugin)) }
-
-    static getParams = (plugin: PluginContext) => {
-        const { types } = plugin.structureRepresentation.registry
-        return {
-            color: PD.Color(ColorNames.cyan),
-            type: PD.MultiSelect(types.map(t => t[0]), types)
-        }
-    }
-
-    componentDidMount() {
-
-    }
-
-    set = (color: Color | -1) => {
-        this.plugin.helpers.structureOverpaint.set(color, this.state.params.type)
-    }
-
-    add = () => {
-        this.set(this.state.params.color)
-    }
-
-    clear = () => {
-        this.set(-1)
-    }
-
-    clearAll = () => {
-        this.plugin.helpers.structureOverpaint.clearAll()
-    }
-
-    render() {
-        return <div className='msp-transform-wrapper'>
-            <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block'>Current Selection Overpaint</button>
-            </div>
-            <div>
-                <ParameterControls params={StructureOverpaintControls.getParams(this.plugin)} values={this.state.params} onChange={p => {
-                    const params = { ...this.state.params, [p.name]: p.value };
-                    this.setState({ params });
-                }}/>
-
-                <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}>Add</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clearAll}>Clear All</button>
-                </div>
-            </div>
-        </div>
-    }
-}

+ 88 - 33
src/mol-plugin/ui/structure/representation.tsx

@@ -6,56 +6,111 @@
 
 import * as React from 'react';
 import { PluginUIComponent } from '../base';
-import { ParamDefinition as PD} from '../../../mol-util/param-definition';
-import { ParameterControls } from '../controls/parameters';
-import { PluginContext } from '../../context';
+import { Structure, StructureElement } from '../../../mol-model/structure';
+import { isEmptyLoci } from '../../../mol-model/loci';
+import { ColorOptions } from '../controls/parameters';
+import { Color } from '../../../mol-util/color';
+import { ButtonSelect, Options } from '../controls/common';
+import { StructureSelectionQueries as Q } from '../../util/structure-selection-helper';
 
-export class StructureRepresentationControls extends PluginUIComponent<{}, { params: PD.Values<ReturnType<typeof StructureRepresentationControls.getParams>> }> {
-    state = { params: PD.getDefaultValues(StructureRepresentationControls.getParams(this.plugin)) }
+abstract class BaseStructureRepresentationControls extends PluginUIComponent {
+    onChange = (value: string) => {
+        console.log('onChange', value)
+    }
 
-    static getParams = (plugin: PluginContext) => {
-        const { types } = plugin.structureRepresentation.registry
-        return {
-            type: PD.Select(types[0][0], types)
-        }
+    abstract label: string
+    abstract lociGetter(structure: Structure): StructureElement.Loci
+
+    show = (value: string) => {
+        this.plugin.helpers.structureRepresentation.set('add', value, this.lociGetter)
     }
 
-    componentDidMount() {
+    hide = (value: string) => {
+        this.plugin.helpers.structureRepresentation.set('remove', value, this.lociGetter)
+    }
 
+    color = (value: string) => {
+        const color = Color(parseInt(value))
+        this.plugin.helpers.structureOverpaint.set(color, this.lociGetter)
     }
 
-    set = (mode: 'add' | 'remove' | 'only' | 'all') => {
-        this.plugin.helpers.structureRepresentation.setSelected(mode, this.state.params.type)
+    render() {
+        const { types } = this.plugin.structureRepresentation.registry
+
+        return <div className='msp-control-row'>
+            <span title={this.label}>{this.label}</span>
+            <div className='msp-select-row'>
+                <ButtonSelect label='Show' onChange={this.show}>
+                    <optgroup label='Show'>
+                        {Options(types)}
+                    </optgroup>
+                </ButtonSelect>
+                <ButtonSelect label='Hide' onChange={this.hide}>
+                    <optgroup label='Clear'>
+                        <option key={-1} value={-1}>TODO: All</option>
+                    </optgroup>
+                    <optgroup label='Hide'>
+                        {Options(types)}
+                    </optgroup>
+                </ButtonSelect>
+                <ButtonSelect label='Color' onChange={this.color}>
+                    <optgroup label='Clear'>
+                        <option key={-1} value={-1}>Theme</option>
+                    </optgroup>
+                    <optgroup label='Color'>
+                        {ColorOptions()}
+                    </optgroup>
+                </ButtonSelect>
+            </div>
+        </div>
     }
+}
+
+class EverythingStructureRepresentationControls extends BaseStructureRepresentationControls {
+    label = 'Everything'
+    lociGetter = (structure: Structure) => {
+        return StructureElement.Loci.all(structure)
+    }
+}
+
+class SelectionStructureRepresentationControls extends BaseStructureRepresentationControls {
+    label = 'Selection'
+    lociGetter = (structure: Structure) => {
+        const loci = this.plugin.helpers.structureSelectionManager.get(structure)
+        return isEmptyLoci(loci) ? StructureElement.Loci.none(structure) : loci
+    }
+}
+
+export class StructureRepresentationControls extends PluginUIComponent {
+    preset = async () => {
+        const { structureSelection: sel, structureRepresentation: rep } = this.plugin.helpers
+        const lociGetter = (structure: Structure) => {
+            const loci = this.plugin.helpers.structureSelectionManager.get(structure)
+            return isEmptyLoci(loci) ? StructureElement.Loci.none(structure) : loci
+        }
+
+        sel.set('add', Q.all())
+        await rep.set('add', 'cartoon', lociGetter)
+        await rep.set('add', 'carbohydrate', lociGetter)
 
-    show = () => { this.set('add') }
-    hide = () => { this.set('remove') }
-    only = () => { this.set('only') }
-    showAll = () => { this.set('all') }
+        sel.set('only', Q.ligandsPlusConnected())
+        sel.set('add', Q.branchedConnectedOnly())
+        sel.set('add', Q.water())
+        await rep.set('add', 'ball-and-stick', lociGetter)
 
-    hideAll = () => {
-        this.plugin.helpers.structureRepresentation.hideAll(this.state.params.type)
+        sel.set('remove', Q.all())
     }
 
     render() {
         return <div className='msp-transform-wrapper'>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block'>Current Selection Representation</button>
+                <button className='msp-btn msp-btn-block'>Representation</button>
             </div>
-            <div>
-                <ParameterControls params={StructureRepresentationControls.getParams(this.plugin)} values={this.state.params} onChange={p => {
-                    const params = { ...this.state.params, [p.name]: p.value };
-                    this.setState({ params });
-                }}/>
-
-                <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.show}>Show</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.hide}>Hide</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.only}>Only</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.showAll}>Show All</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={this.hideAll}>Hide All</button>
-                </div>
+            <div className='msp-btn-row-group'>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.preset()}>Preset</button>
             </div>
+            <EverythingStructureRepresentationControls />
+            <SelectionStructureRepresentationControls />
         </div>
     }
 }

+ 57 - 15
src/mol-plugin/ui/structure/selection.tsx

@@ -6,9 +6,20 @@
 
 import * as React from 'react';
 import { PluginUIComponent } from '../base';
-import { StructureSelection, QueryFn, Queries as _Queries } from '../../../mol-model/structure';
 import { formatStructureSelectionStats } from '../../util/structure-element-selection';
 import { StructureSelectionQueries } from '../../util/structure-selection-helper';
+import { ButtonSelect, Options } from '../controls/common';
+import { PluginCommands } from '../../command';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { Interactivity } from '../../util/interactivity';
+import { ParameterControls } from '../controls/parameters';
+import { camelCaseToWords } from '../../../mol-util/string';
+
+type SelectionModifier = 'add' | 'remove' | 'only'
+
+const StructureSelectionParams = {
+    granularity: Interactivity.Params.granularity,
+}
 
 export class StructureSelectionControls extends PluginUIComponent<{}, {}> {
     state = {}
@@ -17,38 +28,69 @@ export class StructureSelectionControls extends PluginUIComponent<{}, {}> {
         this.subscribe(this.plugin.events.interactivity.selectionUpdated, () => {
             this.forceUpdate()
         });
+
+        this.subscribe(this.plugin.events.interactivity.propsUpdated, () => {
+            this.forceUpdate()
+        });
     }
 
     get stats() {
         return formatStructureSelectionStats(this.plugin.helpers.structureSelectionManager.stats)
     }
 
-    select = (query: QueryFn<StructureSelection>) => {
-        this.plugin.helpers.structureSelection.select(query)
+    setProps = (p: { param: PD.Base<any>, name: string, value: any }) => {
+        if (p.name === 'granularity') {
+            PluginCommands.Interactivity.SetProps.dispatch(this.plugin, { props: { granularity: p.value } });
+        }
     }
 
-    clear = () => {
-        this.plugin.helpers.structureSelection.clearSelection()
+    get values () {
+        return {
+            granularity: this.plugin.interactivity.props.granularity
+        }
     }
 
+    set = (modifier: SelectionModifier, value: string) => {
+        const query = StructureSelectionQueries[value as keyof typeof StructureSelectionQueries]()
+        this.plugin.helpers.structureSelection.set(modifier, query)
+    }
+
+    add = (value: string) => this.set('add', value)
+    remove = (value: string) => this.set('remove', value)
+    only = (value: string) => this.set('only', value)
+
     render() {
+        const queries = Object.keys(StructureSelectionQueries).map(name => {
+            return [name, camelCaseToWords(name)] as [string, string]
+        })
+
         return <div className='msp-transform-wrapper'>
             <div className='msp-transform-header'>
-                <button className='msp-btn msp-btn-block'>Current Selection</button>
+                <button className='msp-btn msp-btn-block'>Selection</button>
             </div>
             <div>
                 <div className='msp-control-row msp-row-text'>
                     <div>{this.stats}</div>
                 </div>
-                <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(StructureSelectionQueries.all())}>All</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.clear()}>None</button>
-                </div>
-                <div className='msp-btn-row-group'>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(StructureSelectionQueries.polymers())}>Polymers</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(StructureSelectionQueries.ligands())}>Ligands</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(StructureSelectionQueries.water())}>Water</button>
-                    <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.select(StructureSelectionQueries.coarse())}>Coarse</button>
+                <ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} />
+                <div className='msp-control-row'>
+                    <div className='msp-select-row' style={{ background: '#f3f2ee' }}>
+                        <ButtonSelect label='Add' onChange={this.add}>
+                            <optgroup label='Add'>
+                                {Options(queries)}
+                            </optgroup>
+                        </ButtonSelect>
+                        <ButtonSelect label='Remove' onChange={this.remove}>
+                            <optgroup label='Remove'>
+                                {Options(queries)}
+                            </optgroup>
+                        </ButtonSelect>
+                        <ButtonSelect label='Only' onChange={this.only}>
+                            <optgroup label='Only'>
+                                {Options(queries)}
+                            </optgroup>
+                        </ButtonSelect>
+                    </div>
                 </div>
             </div>
         </div>

+ 22 - 2
src/mol-plugin/util/interactivity.ts

@@ -90,13 +90,15 @@ namespace Interactivity {
             // TODO clear, then re-apply remaining providers
         }
 
-        normalizedLoci(interactivityLoci: Loci) {
+        normalizedLoci(interactivityLoci: Loci, applyGranularity = true) {
             let { loci, repr } = interactivityLoci
             if (this.props.granularity !== 'element' && Link.isLoci(loci)) {
                 // convert Link.Loci to a StructureElement.Loci so granularity can be applied
                 loci = Link.toStructureElementLoci(loci)
             }
-            loci = Granularity[this.props.granularity](loci)
+            if (applyGranularity) {
+                loci = Granularity[this.props.granularity](loci)
+            }
             if (Structure.isLoci(loci)) {
                 // convert to StructureElement.Loci of root structure
                 loci = Structure.toStructureElementLoci(Structure.Loci(loci.structure.parent || loci.structure))
@@ -197,6 +199,24 @@ namespace Interactivity {
             }
         }
 
+        add(current: Loci<ModelLoci>) {
+            const normalized: Loci<ModelLoci> = this.normalizedLoci(current, false)
+            this.sel.add(normalized.loci);
+            this.mark(normalized, MarkerAction.Select);
+        }
+
+        remove(current: Loci<ModelLoci>) {
+            const normalized: Loci<ModelLoci> = this.normalizedLoci(current, false)
+            this.sel.remove(normalized.loci);
+            this.mark(normalized, MarkerAction.Deselect);
+        }
+
+        only(current: Loci<ModelLoci>) {
+            const sels = this.sel.clear();
+            for (const s of sels) this.mark({ loci: s }, MarkerAction.Deselect);
+            this.add(current);
+        }
+
         constructor(ctx: PluginContext, props: Partial<Props> = {}) {
             super(ctx, props)
             ctx.behaviors.interaction.click.subscribe(e => this.apply(e));

+ 6 - 17
src/mol-plugin/util/structure-overpaint-helper.ts

@@ -42,12 +42,15 @@ export class StructureOverpaintHelper {
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
     }
 
-    async set(color: Color | -1, types?: string[]) {
+    async set(color: Color | -1, lociGetter: (structure: Structure) => StructureElement.Loci, types?: string[]) {
         await this.eachRepr((update, repr, rootStructure, overpaint) => {
             if (types && !types.includes(repr.params!.values.type.name)) return
 
-            const loci = this.plugin.helpers.structureSelectionManager.get(rootStructure)
-            if (isEmptyLoci(loci) || loci.elements.length === 0) return
+            // TODO cleanup when loci is full structure or empty
+            // TODO add & use QueryOverpaintStructureRepresentation3D
+
+            const loci = lociGetter(rootStructure)
+            if (loci.elements.length === 0) return
             const expression = getExpression(loci)
 
             const layer = {
@@ -65,20 +68,6 @@ export class StructureOverpaintHelper {
         })
     }
 
-    add(color: Color, types?: string[]) {
-        this.set(color, types)
-    }
-
-    clear(types?: string[]) {
-        this.set(-1, types)
-    }
-
-    clearAll() {
-        this.eachRepr((update, repr, rootStructure, overpaint) => {
-            if (overpaint) update.delete(overpaint.transform.ref)
-        })
-    }
-
     constructor(private plugin: PluginContext) {
 
     }

+ 6 - 19
src/mol-plugin/util/structure-representation-helper.ts

@@ -7,8 +7,7 @@
 import { PluginStateObject } from '../../mol-plugin/state/objects';
 import { StateTransforms } from '../../mol-plugin/state/transforms';
 import { StateTransformer, StateSelection, StateObjectCell, StateTransform } from '../../mol-state';
-import { StructureElement } from '../../mol-model/structure';
-import { isEmptyLoci } from '../../mol-model/loci';
+import { StructureElement, Structure } from '../../mol-model/structure';
 import { PluginContext } from '../context';
 import { StructureRepresentation3DHelpers } from '../state/transforms/representation';
 
@@ -24,14 +23,13 @@ function getCombinedLoci(mode: SelectionModifier, loci: StructureElement.Loci, c
         case 'add': return StructureElement.Loci.union(loci, currentLoci)
         case 'remove': return StructureElement.Loci.subtract(currentLoci, loci)
         case 'only': return loci
-        case 'all': return StructureElement.Loci.all(loci.structure)
     }
 }
 
-type SelectionModifier = 'add' | 'remove' | 'only' | 'all'
+type SelectionModifier = 'add' | 'remove' | 'only'
 
 export class StructureRepresentationHelper {
-    async set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform) {
+    private async _set(modifier: SelectionModifier, type: string, loci: StructureElement.Loci, structure: StructureTransform) {
         const state = this.plugin.state.dataState
         const update = state.build();
         const s = structure.obj!.data
@@ -67,28 +65,17 @@ export class StructureRepresentationHelper {
         await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
     }
 
-    async setSelected(modifier: SelectionModifier, type: string) {
+    async set(modifier: SelectionModifier, type: string, lociGetter: (structure: Structure) => StructureElement.Loci) {
         const state = this.plugin.state.dataState;
         const structures = state.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure));
 
         for (const structure of structures) {
             const s = structure.obj!.data
-            const _loci = this.plugin.helpers.structureSelectionManager.get(s)
-            const loci = isEmptyLoci(_loci) ? StructureElement.Loci(s, []) : _loci
-
-            await this.set(modifier, type, loci, structure)
+            const loci = lociGetter(s)
+            await this._set(modifier, type, loci, structure)
         }
     }
 
-    async hideAll(type: string) {
-        const state = this.plugin.state.dataState;
-        const update = state.build();
-
-        state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure).withTag(getRepresentationManagerTag(type))).forEach(structure => update.delete(structure.transform.ref));
-
-        await this.plugin.runTask(state.updateTree(update, { doNotUpdateCurrent: true }));
-    }
-
     constructor(private plugin: PluginContext) {
 
     }

+ 111 - 26
src/mol-plugin/util/structure-selection-helper.ts

@@ -7,48 +7,133 @@
 import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
 import { StateSelection } from '../../mol-state';
 import { PluginStateObject } from '../state/objects';
-import { QueryContext, StructureSelection, QueryFn, Queries as _Queries } from '../../mol-model/structure';
+import { QueryContext, StructureSelection, QueryFn } from '../../mol-model/structure';
 import { compile } from '../../mol-script/runtime/query/compiler';
-import { ButtonsType } from '../../mol-util/input/input-observer';
-import { EmptyLoci } from '../../mol-model/loci';
+import { Loci } from '../../mol-model/loci';
 import { PluginContext } from '../context';
 
+const polymers = MS.struct.modifier.union([
+    MS.struct.generator.atomGroups({
+        'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer'])
+    })
+])
+
+const backboneTrace = MS.struct.modifier.union([
+    MS.struct.generator.atomGroups({
+        'atom-test': MS.core.logic.or([
+            MS.core.rel.eq([MS.ammp('label_atom_id'), 'CA']),
+            MS.core.rel.eq([MS.ammp('label_atom_id'), 'P'])
+        ])
+    })
+])
+
+const water = MS.struct.modifier.union([
+    MS.struct.generator.atomGroups({
+        'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'water'])
+    })
+])
+
+const branched = MS.struct.modifier.union([
+    MS.struct.combinator.merge([
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'branched'])
+            })
+        ]),
+        MS.struct.modifier.union([
+            MS.struct.generator.atomGroups({
+                'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer']),
+                'residue-test': MS.core.str.match([MS.re('saccharide', 'i'), MS.ammp('chemCompType')])
+            })
+        ])
+    ])
+])
+
+const branchedPlusConnected = MS.struct.modifier.union([
+    MS.struct.modifier.includeConnected({
+        0: branched, 'layer-count': 1, 'as-whole-residues': true
+    })
+])
+
+const branchedConnectedOnly = MS.struct.modifier.union([
+    MS.struct.modifier.exceptBy({
+        0: branchedPlusConnected,
+        by: branched
+    })
+])
+
+const ligands = MS.struct.modifier.union([
+    MS.struct.generator.atomGroups({
+        'entity-test': MS.core.logic.and([
+            MS.core.rel.neq([MS.ammp('entityType'), 'branched']),
+            MS.core.rel.eq([MS.ammp('entityType'), 'non-polymer'])
+        ]),
+        'residue-test': MS.core.logic.not([
+            MS.core.str.match([MS.re('saccharide', 'i'), MS.ammp('chemCompType')])
+        ])
+    })
+])
+
+const ligandsPlusConnected = MS.struct.modifier.union([
+    MS.struct.modifier.includeConnected({
+        0: ligands, 'layer-count': 1, 'as-whole-residues': true
+    })
+])
+
+const coarse = MS.struct.modifier.union([
+    MS.struct.generator.atomGroups({
+        'chain-test': MS.core.set.has([
+            MS.set('sphere', 'gaussian'), MS.ammp('objectPrimitive')
+        ])
+    })
+])
+
 export const StructureSelectionQueries = {
     all: () => compile<StructureSelection>(MS.struct.generator.all()),
-    polymers: () => _Queries.internal.atomicSequence(),
-    water: () => _Queries.internal.water(),
-    ligands: () => _Queries.internal.atomicHet(),
-    coarse: () => _Queries.internal.spheres(),
+    polymers: () => compile<StructureSelection>(polymers),
+    backboneTrace: () => compile<StructureSelection>(backboneTrace),
+    water: () => compile<StructureSelection>(water),
+    branched: () => compile<StructureSelection>(branched),
+    branchedPlusConnected: () => compile<StructureSelection>(branchedPlusConnected),
+    branchedConnectedOnly: () => compile<StructureSelection>(branchedConnectedOnly),
+    ligands: () => compile<StructureSelection>(ligands),
+    ligandsPlusConnected: () => compile<StructureSelection>(ligandsPlusConnected),
+    coarse: () => compile<StructureSelection>(coarse),
 }
 
+//
+
+type SelectionModifier = 'add' | 'remove' | 'only'
+
 export class StructureSelectionHelper {
-    select(query: QueryFn<StructureSelection>) {
+    private get structures() {
         const state = this.plugin.state.dataState
-        const structures = state.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure))
+        return state.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure))
+    }
+
+    private _set(modifier: SelectionModifier, loci: Loci) {
+        switch (modifier) {
+            case 'add':
+                this.plugin.interactivity.lociSelections.add({ loci })
+                break
+            case 'remove':
+                this.plugin.interactivity.lociSelections.remove({ loci })
+                break
+            case 'only':
+                this.plugin.interactivity.lociSelections.only({ loci })
+                break
+        }
+    }
 
-        for (const so of structures) {
+    set(modifier: SelectionModifier, query: QueryFn<StructureSelection>) {
+        for (const so of this.structures) {
             const s = so.obj!.data
             const result = query(new QueryContext(s))
             const loci = StructureSelection.toLoci2(result)
-
-            // TODO use better API when available
-            this.plugin.interactivity.lociSelections.apply({
-                current: { loci },
-                buttons: ButtonsType.Flag.Secondary,
-                modifiers: { shift: false, alt: false, control: true, meta: false }
-            })
+            this._set(modifier, loci)
         }
     }
 
-    clearSelection() {
-        // TODO use better API when available
-        this.plugin.interactivity.lociSelections.apply({
-            current: { loci: EmptyLoci },
-            buttons: ButtonsType.Flag.Secondary,
-            modifiers: { shift: false, alt: false, control: true, meta: false }
-        })
-    }
-
     constructor(private plugin: PluginContext) {
 
     }