Browse Source

wip mol-plugin managers

David Sehnal 5 years ago
parent
commit
b2743c76e0

+ 13 - 13
src/mol-plugin-state/actions/structure.ts

@@ -37,7 +37,7 @@ export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | P
         return Task.create('mmCIF default builder', async taskCtx => {
             const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'mmcif' });
             if (options.visuals) {
-                await ctx.builders.representation.structurePreset(structure, 'auto');
+                await ctx.builders.structure.representation.structurePreset(structure, 'auto');
             }
         })
     }
@@ -55,7 +55,7 @@ export const PdbProvider: DataFormatProvider<any> = {
         return Task.create('PDB default builder', async () => {
             const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'pdb' });
             if (options.visuals) {
-                await ctx.builders.representation.structurePreset(structure, 'auto');
+                await ctx.builders.structure.representation.structurePreset(structure, 'auto');
             }
         })
     }
@@ -73,7 +73,7 @@ export const GroProvider: DataFormatProvider<any> = {
         return Task.create('GRO default builder', async () => {
             const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'gro' });
             if (options.visuals) {
-                await ctx.builders.representation.structurePreset(structure, 'auto');
+                await ctx.builders.structure.representation.structurePreset(structure, 'auto');
             }
         })
     }
@@ -91,7 +91,7 @@ export const Provider3dg: DataFormatProvider<any> = {
         return Task.create('3DG default builder', async () => {
             const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: '3dg' });
             if (options.visuals) {
-                await ctx.builders.representation.structurePreset(structure, 'auto');
+                await ctx.builders.structure.representation.structurePreset(structure, 'auto');
             }
         })
     }
@@ -249,7 +249,7 @@ const DownloadStructure = StateAction.build({
                 structureProperties: supportProps
             });
             if (createRepr) {
-                await plugin.builders.representation.structurePreset(structure, 'auto');
+                await plugin.builders.structure.representation.structurePreset(structure, 'auto');
             }
         } else {
             for (const download of downloadParams) {
@@ -261,7 +261,7 @@ const DownloadStructure = StateAction.build({
                     structureProperties: supportProps
                 });
                 if (createRepr) {
-                    await plugin.builders.representation.structurePreset(structure, 'auto');
+                    await plugin.builders.structure.representation.structurePreset(structure, 'auto');
                 }
             }
         }
@@ -303,28 +303,28 @@ export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary
 export const Create3DRepresentationPreset = StateAction.build({
     display: { name: '3D Representation Preset', description: 'Create one of preset 3D representations.' },
     from: PluginStateObject.Molecule.Structure,
-    isApplicable(a, _, plugin: PluginContext) { return plugin.builders.representation.hasPreset(a.data); },
+    isApplicable(a, _, plugin: PluginContext) { return plugin.builders.structure.representation.hasPreset(a.data); },
     params(a, plugin: PluginContext) {
         return {
-            type: plugin.builders.representation.getPresets(a.data)
+            type: plugin.builders.structure.representation.getPresets(a.data)
         };
     }
 })(({ ref, params }, plugin: PluginContext) => {
-    plugin.builders.representation.structurePreset(ref, params.type.name, params.type.params);
+    plugin.builders.structure.representation.structurePreset(ref, params.type.name, params.type.params);
 });
 
 export const Remove3DRepresentationPreset = StateAction.build({
     display: { name: 'Remove 3D Representation Preset', description: 'Remove 3D representations.' },
     from: PluginStateObject.Molecule.Structure,
-    isApplicable(_, t, plugin: PluginContext) { return plugin.builders.representation.hasPresetRepresentation(t.ref); },
+    isApplicable(_, t, plugin: PluginContext) { return plugin.builders.structure.representation.hasPresetRepresentation(t.ref); },
     params(a, plugin: PluginContext) {
         return {
-            type: plugin.builders.representation.getPresets(a.data).select
+            type: plugin.builders.structure.representation.getPresets(a.data).select
         };
     }
 })(({ ref, params }, plugin: PluginContext) => {
     // TODO: this will be completely handled by the managed and is just for testing purposes
-    plugin.builders.representation.removePreset(params.type, ref);
+    plugin.builders.structure.representation.removePreset(params.type, ref);
 });
 
 export const UpdateTrajectory = StateAction.build({
@@ -440,6 +440,6 @@ export const AddTrajectory = StateAction.build({
 
         await state.updateTree(model).runInContext(taskCtx);
         const structure = await ctx.builders.structure.createStructure(model.selector);
-        await ctx.builders.representation.structurePreset(structure, 'auto');
+        await ctx.builders.structure.representation.structurePreset(structure, 'auto');
     }).runInContext(taskCtx)
 }));

+ 0 - 146
src/mol-plugin-state/builder/representation.ts

@@ -1,146 +0,0 @@
-/**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { arrayFind } from '../../mol-data/util';
-import { Structure } from '../../mol-model/structure';
-import { StateTransform, StateTree, StateSelection, StateObjectRef } from '../../mol-state';
-import { Task } from '../../mol-task';
-import { isProductionMode } from '../../mol-util/debug';
-import { objectForEach } from '../../mol-util/object';
-import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { PluginContext } from '../../mol-plugin/context';
-import { PresetStructureReprentations } from './structure/preset';
-import { StructureRepresentationProvider, RepresentationProviderTags } from './structure/provider';
-import { UniqueArray } from '../../mol-data/generic';
-
-// TODO: support quality
-// TODO: support ignore hydrogens
-
-export type RepresentationProviderRef = keyof PresetStructureReprentations | StructureRepresentationProvider | string
-
-export class RepresentationBuilder {
-    private providers: StructureRepresentationProvider[] = [];
-    private providerMap: Map<string, StructureRepresentationProvider> = new Map();
-
-    readonly defaultProvider = PresetStructureReprentations.auto;
-
-    private resolveProvider(ref: RepresentationProviderRef) {
-        return typeof ref === 'string'
-            ? PresetStructureReprentations[ref as keyof PresetStructureReprentations] ?? arrayFind(this.providers, p => p.id === ref)
-            : ref;
-    }
-
-    hasPreset(s: Structure) {
-        for (const p of this.providers) {
-            if (!p.isApplicable || p.isApplicable(s, this.plugin)) return true;
-        }
-        return false;
-    }
-
-    getPresets(s: Structure) {
-        const options: [string, string][] = [];
-        const map: { [K in string]: PD.Any } = Object.create(null);
-        for (const p of this.providers) {
-            if (p.isApplicable && !p.isApplicable(s, this.plugin)) continue;
-
-            options.push([p.id, p.display.name]);
-            map[p.id] = p.params ? PD.Group(p.params(s, this.plugin)) : PD.EmptyGroup()
-        }
-        if (options.length === 0) return PD.MappedStatic('', { '': PD.EmptyGroup() });
-        return PD.MappedStatic(options[0][0], map, { options });
-    }
-
-    hasPresetRepresentation(ref: StateObjectRef) {
-        // TODO: make this state selection function?
-        const tree = this.plugin.state.dataState.tree;
-        const root = StateObjectRef.resolve(this.plugin.state.dataState, ref);
-        if (!root) return false;
-        return StateTree.doPreOrder(tree, root.transform,  { found: false, map: this.providerMap }, (n, _, s) => {
-            if (!n.tags) return;
-            for (const t of n.tags) {
-                if (s.map.has(t)) {
-                    s.found = true;
-                    return false;
-                }
-            }
-        }).found;
-    }
-
-    getPresetRepresentations(ref: StateObjectRef) {
-        const tree = this.plugin.state.dataState.tree;
-        const root = StateObjectRef.resolve(this.plugin.state.dataState, ref);
-        if (!root) return [];
-        return StateTree.doPreOrder(tree, root.transform, { found: UniqueArray.create<string, StructureRepresentationProvider>(), map: this.providerMap }, (n, _, s) => {
-            if (!n.tags) return;
-            for (const t of n.tags) {
-                if (s.map.has(t)) UniqueArray.add(s.found, t, s.map.get(t)!);
-            }
-        }).found.array;
-    }
-
-    registerPreset(provider: StructureRepresentationProvider) {
-        if (this.providerMap.has(provider.id)) {
-            throw new Error(`Repr. provider with id '${provider.id}' already registered.`);
-        }
-        // TODO: sort by group
-        this.providers.push(provider);
-        this.providerMap.set(provider.id, provider);
-    }
-
-    removePreset(providerRef: RepresentationProviderRef, structureRoot?: StateObjectRef) {
-        const id = this.resolveProvider(providerRef)?.id;
-        if (!id) return;
-
-        const state = this.plugin.state.dataState;
-        const root = StateObjectRef.resolveRef(state, structureRoot) || StateTransform.RootRef;
-        const reprs = StateSelection.findWithAllTags(state.tree, root, new Set([id, RepresentationProviderTags.Representation]));
-
-        const builder = state.build();
-        for (const r of reprs) {
-            builder.delete(r.ref);
-        }
-
-        const tree = builder.currentTree;
-        const selections = StateSelection.findWithAllTags(tree, root, new Set([RepresentationProviderTags.Component]));
-
-        for (const s of selections) {
-            if (!tree.children.has(s.ref) || tree.children.get(s.ref).size === 0) builder.delete(s.ref);
-        }
-
-        if (builder.editInfo.count === 0) return;
-        return this.plugin.runTask(state.updateTree(builder));
-    }
-
-    structurePreset<K extends keyof PresetStructureReprentations>(parent: StateObjectRef, preset: K, params?: StructureRepresentationProvider.Params<PresetStructureReprentations[K]>): Promise<StructureRepresentationProvider.State<PresetStructureReprentations[K]>> | undefined
-    structurePreset<P = any, S = {}>(parent: StateObjectRef, providers: StructureRepresentationProvider<P, S>, params?: P): Promise<S> | undefined
-    structurePreset(parent: StateObjectRef, providerId: string, params?: any): Promise<any> | undefined
-    structurePreset(parent: StateObjectRef, providerRef: string | StructureRepresentationProvider, params?: any): Promise<any> | undefined {
-        const provider = this.resolveProvider(providerRef);
-        if (!provider) return;
-
-        const state = this.plugin.state.dataState;
-        const cell = StateObjectRef.resolveAndCheck(state, parent);
-        if (!cell) {
-            if (!isProductionMode) console.warn(`Applying structure repr. provider to bad cell.`);
-            return;
-        }
-
-        const prms = params || (provider.params
-            ? PD.getDefaultValues(provider.params(cell.obj!.data, this.plugin))
-            : {})
-
-
-        const task = Task.create(`${provider.display.name}`, ctx => provider.apply(ctx, state, cell, prms, this.plugin) as Promise<any>);
-        return this.plugin.runTask(task);
-    }
-
-    // TODO
-    // createOrUpdate(component: any, ) { }
-
-    constructor(public plugin: PluginContext) {
-        objectForEach(PresetStructureReprentations, r => this.registerPreset(r));
-    }
-}

+ 3 - 0
src/mol-plugin-state/builder/structure.ts

@@ -11,6 +11,7 @@ import { StateTransforms } from '../transforms';
 import { RootStructureDefinition } from '../helpers/root-structure';
 import { StructureComponentParams } from '../helpers/structure-component';
 import { BuildInTrajectoryFormat, TrajectoryFormatProvider } from '../formats/trajectory';
+import { StructureRepresentationBuilder } from './structure/representation';
 
 export type TrajectoryFormat = 'pdb' | 'cif' | 'gro' | '3dg'
 
@@ -44,6 +45,8 @@ export class StructureBuilder {
         return trajectory.selector;
     }
 
+    readonly representation = new StructureRepresentationBuilder(this.plugin);
+
     async parseStructure(params: {
         data?: StateObjectRef<SO.Data.Binary | SO.Data.String>,
         dataFormat?: BuildInTrajectoryFormat | TrajectoryFormatProvider,

+ 148 - 0
src/mol-plugin-state/builder/structure/representation.ts

@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { arrayFind } from '../../../mol-data/util';
+import { Structure } from '../../../mol-model/structure';
+import { StateTransform, StateTree, StateSelection, StateObjectRef } from '../../../mol-state';
+import { Task } from '../../../mol-task';
+import { isProductionMode } from '../../../mol-util/debug';
+import { objectForEach } from '../../../mol-util/object';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { PluginContext } from '../../../mol-plugin/context';
+import { PresetStructureReprentations } from './preset';
+import { StructureRepresentationProvider, RepresentationProviderTags } from './provider';
+import { UniqueArray } from '../../../mol-data/generic';
+
+// TODO: support quality
+// TODO: support ignore hydrogens
+
+export type StructureRepresentationProviderRef = keyof PresetStructureReprentations | StructureRepresentationProvider | string
+
+export class StructureRepresentationBuilder {
+    private providers: StructureRepresentationProvider[] = [];
+    private providerMap: Map<string, StructureRepresentationProvider> = new Map();
+
+    readonly defaultProvider = PresetStructureReprentations.auto;
+
+    private resolveProvider(ref: StructureRepresentationProviderRef) {
+        return typeof ref === 'string'
+            ? PresetStructureReprentations[ref as keyof PresetStructureReprentations] ?? arrayFind(this.providers, p => p.id === ref)
+            : ref;
+    }
+
+    hasPreset(s: Structure) {
+        for (const p of this.providers) {
+            if (!p.isApplicable || p.isApplicable(s, this.plugin)) return true;
+        }
+        return false;
+    }
+
+    get providerList(): ReadonlyArray<StructureRepresentationProvider> { return this.providers; }
+
+    getPresets(s: Structure) {
+        const options: [string, string][] = [];
+        const map: { [K in string]: PD.Any } = Object.create(null);
+        for (const p of this.providers) {
+            if (p.isApplicable && !p.isApplicable(s, this.plugin)) continue;
+
+            options.push([p.id, p.display.name]);
+            map[p.id] = p.params ? PD.Group(p.params(s, this.plugin)) : PD.EmptyGroup()
+        }
+        if (options.length === 0) return PD.MappedStatic('', { '': PD.EmptyGroup() });
+        return PD.MappedStatic(options[0][0], map, { options });
+    }
+
+    hasPresetRepresentation(ref: StateObjectRef) {
+        // TODO: make this state selection function?
+        const tree = this.plugin.state.dataState.tree;
+        const root = StateObjectRef.resolve(this.plugin.state.dataState, ref);
+        if (!root) return false;
+        return StateTree.doPreOrder(tree, root.transform,  { found: false, map: this.providerMap }, (n, _, s) => {
+            if (!n.tags) return;
+            for (const t of n.tags) {
+                if (s.map.has(t)) {
+                    s.found = true;
+                    return false;
+                }
+            }
+        }).found;
+    }
+
+    getPresetRepresentations(ref: StateObjectRef) {
+        const tree = this.plugin.state.dataState.tree;
+        const root = StateObjectRef.resolve(this.plugin.state.dataState, ref);
+        if (!root) return [];
+        return StateTree.doPreOrder(tree, root.transform, { found: UniqueArray.create<string, StructureRepresentationProvider>(), map: this.providerMap }, (n, _, s) => {
+            if (!n.tags) return;
+            for (const t of n.tags) {
+                if (s.map.has(t)) UniqueArray.add(s.found, t, s.map.get(t)!);
+            }
+        }).found.array;
+    }
+
+    registerPreset(provider: StructureRepresentationProvider) {
+        if (this.providerMap.has(provider.id)) {
+            throw new Error(`Repr. provider with id '${provider.id}' already registered.`);
+        }
+        // TODO: sort by group
+        this.providers.push(provider);
+        this.providerMap.set(provider.id, provider);
+    }
+
+    removePreset(providerRef: StructureRepresentationProviderRef, structureRoot?: StateObjectRef) {
+        const id = this.resolveProvider(providerRef)?.id;
+        if (!id) return;
+
+        const state = this.plugin.state.dataState;
+        const root = StateObjectRef.resolveRef(state, structureRoot) || StateTransform.RootRef;
+        const reprs = StateSelection.findWithAllTags(state.tree, root, new Set([id, RepresentationProviderTags.Representation]));
+
+        const builder = state.build();
+        for (const r of reprs) {
+            builder.delete(r.ref);
+        }
+
+        const tree = builder.currentTree;
+        const selections = StateSelection.findWithAllTags(tree, root, new Set([RepresentationProviderTags.Component]));
+
+        for (const s of selections) {
+            if (!tree.children.has(s.ref) || tree.children.get(s.ref).size === 0) builder.delete(s.ref);
+        }
+
+        if (builder.editInfo.count === 0) return;
+        return this.plugin.runTask(state.updateTree(builder));
+    }
+
+    structurePreset<K extends keyof PresetStructureReprentations>(parent: StateObjectRef, preset: K, params?: StructureRepresentationProvider.Params<PresetStructureReprentations[K]>): Promise<StructureRepresentationProvider.State<PresetStructureReprentations[K]>> | undefined
+    structurePreset<P = any, S = {}>(parent: StateObjectRef, providers: StructureRepresentationProvider<P, S>, params?: P): Promise<S> | undefined
+    structurePreset(parent: StateObjectRef, providerId: string, params?: any): Promise<any> | undefined
+    structurePreset(parent: StateObjectRef, providerRef: string | StructureRepresentationProvider, params?: any): Promise<any> | undefined {
+        const provider = this.resolveProvider(providerRef);
+        if (!provider) return;
+
+        const state = this.plugin.state.dataState;
+        const cell = StateObjectRef.resolveAndCheck(state, parent);
+        if (!cell) {
+            if (!isProductionMode) console.warn(`Applying structure repr. provider to bad cell.`);
+            return;
+        }
+
+        const prms = params || (provider.params
+            ? PD.getDefaultValues(provider.params(cell.obj!.data, this.plugin))
+            : {})
+
+
+        const task = Task.create(`${provider.display.name}`, ctx => provider.apply(ctx, state, cell, prms, this.plugin) as Promise<any>);
+        return this.plugin.runTask(task);
+    }
+
+    // TODO
+    // createOrUpdate(component: any, ) { }
+
+    constructor(public plugin: PluginContext) {
+        objectForEach(PresetStructureReprentations, r => this.registerPreset(r));
+    }
+}

+ 12 - 4
src/mol-plugin-state/helpers/structure-selection-query.ts

@@ -43,14 +43,16 @@ interface StructureSelectionQuery {
     readonly description: string
     readonly category: string
     readonly isHidden: boolean
+    readonly referencesCurrent: boolean
     readonly query: StructureQuery
     readonly ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
 }
 
 interface StructureSelectionQueryProps {
-    description?: string,
+    description?: string
     category?: string
     isHidden?: boolean
+    referencesCurrent?: boolean
     ensureCustomProperties?: (ctx: CustomProperty.Context, structure: Structure) => Promise<void>
 }
 
@@ -62,6 +64,7 @@ function StructureSelectionQuery(label: string, expression: Expression, props: S
         description: props.description || '',
         category: props.category ?? StructureSelectionCategory.Misc,
         isHidden: !!props.isHidden,
+        referencesCurrent: !!props.referencesCurrent,
         get query() {
             if (!_query) _query = compile<StructureSelection>(expression)
             return _query
@@ -71,6 +74,7 @@ function StructureSelectionQuery(label: string, expression: Expression, props: S
 }
 
 const all = StructureSelectionQuery('All', MS.struct.generator.all(), { category: '' })
+const current = StructureSelectionQuery('Current Selection', MS.internal.generator.current(), { category: '', referencesCurrent: true })
 
 const polymer = StructureSelectionQuery('Polymer', MS.struct.modifier.union([
     MS.struct.generator.atomGroups({
@@ -343,7 +347,8 @@ const surroundings = StructureSelectionQuery('Surrounding Residues (5 \u212B) of
     })
 ]), {
     description: 'Select residues within 5 \u212B of the current selection.',
-    category: StructureSelectionCategory.Manipulate
+    category: StructureSelectionCategory.Manipulate,
+    referencesCurrent: true
 })
 
 const complement = StructureSelectionQuery('Inverse / Complement of Selection', MS.struct.modifier.union([
@@ -353,7 +358,8 @@ const complement = StructureSelectionQuery('Inverse / Complement of Selection',
     })
 ]), {
     description: 'Select everything not in the current selection.',
-    category: StructureSelectionCategory.Manipulate
+    category: StructureSelectionCategory.Manipulate,
+    referencesCurrent: true
 })
 
 const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct.modifier.union([
@@ -362,7 +368,8 @@ const bonded = StructureSelectionQuery('Residues Bonded to Selection', MS.struct
     })
 ]), {
     description: 'Select residues covalently bonded to current selection.',
-    category: StructureSelectionCategory.Manipulate
+    category: StructureSelectionCategory.Manipulate,
+    referencesCurrent: true
 })
 
 const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modifier.union([
@@ -459,6 +466,7 @@ function ResidueQuery([names, label]: [string[], string], category: string) {
 
 export const StructureSelectionQueries = {
     all,
+    current,
     polymer,
     trace,
     backbone,

+ 65 - 0
src/mol-plugin-state/manager/structure/component.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureRef } from './hierarchy-state'
+import { StructureRepresentationProvider } from '../../builder/structure/provider';
+import { PluginContext } from '../../../mol-plugin/context';
+
+export { StructureComponentManager }
+
+class StructureComponentManager {
+    applyPreset<P = any, S = {}>(structures: StructureRef[], provider: StructureRepresentationProvider<P, S>, params?: P): Promise<any>  {
+        return this.plugin.runTask(this.dateState.transaction(async () => {
+            await this.removeComponents(structures);
+            for (const s of structures) {
+                await this.plugin.builders.structure.representation.structurePreset(s.cell, provider, params);
+            }
+        }));
+    }
+
+    clear(structures: StructureRef[]) {
+        return this.removeComponents(structures);
+    }
+
+    modify(structures: StructureRef[], action: StructureComponentManager.ModifyAction) {
+
+    }
+
+    private get dateState() {
+        return this.plugin.state.dataState;
+    }
+
+    private removeComponents(structures: StructureRef[]) {
+        const deletes = this.dateState.build();
+        for (const s of structures) {
+            for (const c of s.components) {
+                deletes.delete(c.cell.transform.ref);
+            }
+            if (s.currentFocus) {
+                if (s.currentFocus.focus) deletes.delete(s.currentFocus.focus.cell.transform.ref);
+                if (s.currentFocus.surroundings) deletes.delete(s.currentFocus.surroundings.cell.transform.ref);
+            }
+        }
+        return this.plugin.runTask(this.dateState.updateTree(deletes));
+    }
+
+    constructor(public plugin: PluginContext) {
+
+    }
+}
+
+namespace StructureComponentManager {
+
+    export function getModifyParams() {
+        return 0 as any;
+    }
+
+    export type ModifyAction = 
+        | { kind: 'add', label: string, representationType?: string }
+        | { kind: 'merge', type: { kind: 'intersecting', key: string } | { kind: 'component', key: string } }
+        | { kind: 'subtract', type: { kind: 'all' } | { kind: 'component', key: string } }
+        | { kind: 'color', representationType?: string }
+}

+ 0 - 7
src/mol-plugin-state/manager/structure/components.ts

@@ -1,7 +0,0 @@
-/**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-// TODO: manager that handles structure selections

+ 7 - 1
src/mol-plugin-state/manager/structure/hierarchy-state.ts

@@ -90,11 +90,17 @@ function StructurePropertiesRef(cell: StateObjectCell<SO.Molecule.Structure>, st
 
 export interface StructureComponentRef extends RefBase<'structure-component', SO.Molecule.Structure> {
     structure: StructureRef,
+    key?: string,
     representations: StructureRepresentationRef[],
 }
 
+function componentKey(cell: StateObjectCell<SO.Molecule.Structure>) {
+    if (!cell.transform.tags) return;
+    return [...cell.transform.tags].sort().join();
+}
+
 function StructureComponentRef(cell: StateObjectCell<SO.Molecule.Structure>, structure: StructureRef): StructureComponentRef {
-    return { kind: 'structure-component', cell, version: cell.transform.version, structure, representations: [] };
+    return { kind: 'structure-component', cell, version: cell.transform.version, structure, key: componentKey(cell), representations: [] };
 }
 
 export interface StructureRepresentationRef extends RefBase<'structure-representation', SO.Molecule.Structure.Representation3D> {

+ 2 - 7
src/mol-plugin-state/manager/structure/hierarchy.ts

@@ -74,11 +74,6 @@ export class StructureHierarchyManager extends PluginComponent<StructureHierarch
 }
 
 export namespace StructureHierarchyManager {
-    function componentKey(c: StructureComponentRef) {
-        if (!c.cell.transform.tags) return;
-        return [...c.cell.transform.tags].sort().join();
-    }
-
     export function getCommonComponentPivots(models: ReadonlyArray<ModelRef>) {
         if (!models[0]?.structures?.length) return [];
         if (models[0]?.structures?.length === 1) return models[0]?.structures[0]?.components || [];
@@ -86,7 +81,7 @@ export namespace StructureHierarchyManager {
         const pivots = new Map<string, StructureComponentRef>();
 
         for (const c of models[0]?.structures[0]?.components) {
-            const key = componentKey(c);
+            const key = c.key;
             if (!key) continue;
             pivots.set(key, c);
         }
@@ -94,7 +89,7 @@ export namespace StructureHierarchyManager {
         for (const m of models) {
             for (const s of m.structures) {
                 for (const c of s.components) {
-                    const key = componentKey(c);
+                    const key = c.key;
                     if (!key) continue;
                     if (!pivots.has(key)) pivots.delete(key);
                 }

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

@@ -279,7 +279,6 @@ export function IconButton(props: {
 }
 
 export class ButtonSelect extends React.PureComponent<{ label: string, onChange: (value: string) => void, disabled?: boolean }> {
-
     onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
         e.preventDefault()
         this.props.onChange(e.target.value)

+ 84 - 5
src/mol-plugin-ui/structure/components.tsx

@@ -10,11 +10,12 @@ import { StructureHierarchyManager } from '../../mol-plugin-state/manager/struct
 import { StructureComponentRef, StructureRepresentationRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { State, StateAction } from '../../mol-state';
 import { PluginCommands } from '../../mol-plugin/commands';
-import { ExpandGroup, IconButton, ControlGroup } from '../controls/common';
+import { ExpandGroup, IconButton, ControlGroup, ToggleButton } from '../controls/common';
 import { UpdateTransformControl } from '../state/update-transform';
 import { ActionMenu } from '../controls/action-menu';
 import { ApplyActionControl } from '../state/apply-action';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { Icon } from '../controls/icons';
 
 interface StructureComponentControlState extends CollapsableState {
     isDisabled: boolean
@@ -31,6 +32,86 @@ export class StructureComponentControls extends CollapsableControls<{}, Structur
         return { header: 'Components', isCollapsed: false, isDisabled: false };
     }
 
+    renderControls() {
+        return <>
+            <ComponentEditorControls />
+            <ComponentListControls />
+        </>;
+    }
+}
+
+interface ComponentEditorControlsState {
+    action?: 'preset' | 'modify' | 'options',
+    isDisabled: boolean
+}
+
+class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorControlsState> {
+    state: ComponentEditorControlsState = {
+        isDisabled: false
+    };
+
+    private toggleAction(action: ComponentEditorControlsState['action']) {
+        return () => this.setState({ action: this.state.action === action ? void 0 : action });
+    }
+
+    togglePreset = this.toggleAction('preset');
+    toggleModify = this.toggleAction('modify');
+    toggleOptions = this.toggleAction('options');
+    hideAction = () => this.setState({ action: void 0 });
+
+    modifyControls = {
+        add: () => { },
+    }
+
+    modifySelect() {
+        return <div className='msp-control-row msp-select-row'>
+            <button><Icon name='plus' /> Add</button>
+            <button><Icon name='flow-branch' /> Merge</button>
+            <button><Icon name='minus' /> Sub</button>
+            <button><Icon name='brush' /> Color</button>
+        </div>;
+    }
+
+    get presetControls() {
+        return <ActionMenu items={this.presetActions} onSelect={this.applyPreset} />
+    }
+
+    get presetActions() {
+        const actions = [
+            ActionMenu.Item('Clear', null),
+        ];
+        // TODO: filter by applicable
+        for (const p of this.plugin.builders.structure.representation.providerList) {
+            actions.push(ActionMenu.Item(p.display.name, p));
+        }
+        return actions;
+    }
+
+    applyPreset: ActionMenu.OnSelect = item => {
+        this.hideAction();
+
+        if (!item) return;
+        const mng = this.plugin.managers.structure;
+
+        // TODO: proper list
+        const structures = mng.hierarchy.state.currentModels[0].structures;
+        if (item.value === null) mng.component.clear(structures);
+        else mng.component.applyPreset(this.plugin.managers.structure.hierarchy.state.currentModels[0].structures, item.value as any);
+    }
+
+    render() {
+        return <>
+            <div className='msp-control-row msp-select-row'>
+                <ToggleButton icon='bookmarks' label='Preset' toggle={this.togglePreset} isSelected={this.state.action === 'preset'} disabled={this.state.isDisabled} />
+                <ToggleButton icon='flow-cascade' label='Modify' toggle={this.toggleModify} isSelected={this.state.action === 'modify'} disabled={this.state.isDisabled} />
+                <ToggleButton icon='cog' label='Options' toggle={this.toggleOptions} isSelected={this.state.action === 'options'} disabled={this.state.isDisabled} />
+            </div>
+            {this.state.action === 'preset' && this.presetControls}
+        </>;
+    }
+}
+
+class ComponentListControls extends PurePluginUIComponent {
     get currentModels() {
         return this.plugin.managers.structure.hierarchy.behaviors.currentModels;
     }
@@ -39,11 +120,9 @@ export class StructureComponentControls extends CollapsableControls<{}, Structur
         this.subscribe(this.currentModels, () => this.forceUpdate());
     }
 
-    renderControls() {
+    render() {
         const components = StructureHierarchyManager.getCommonComponentPivots(this.currentModels.value)
-        return <>
-            {components.map(c => <StructureComponentEntry key={c.cell.transform.ref} component={c} />)}
-        </>;
+        return components.map(c => <StructureComponentEntry key={c.cell.transform.ref} component={c} />)
     }
 }
 

+ 7 - 7
src/mol-plugin-ui/structure/selection.tsx

@@ -122,9 +122,9 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
     get controls() {
         return <div>
             <div className='msp-control-row msp-select-row'>
-                <ToggleButton icon='plus' label='Select' toggle={this.toggleAdd} isSelected={this.state.queryAction === 'add'} disabled={this.state.isDisabled} />
-                <ToggleButton icon='minus' label='Deselect' toggle={this.toggleRemove} isSelected={this.state.queryAction === 'remove'} disabled={this.state.isDisabled} />
-                <ToggleButton icon='flash' label='Only' toggle={this.toggleOnly} isSelected={this.state.queryAction === 'set'} disabled={this.state.isDisabled} />
+                <ToggleButton icon='plus' label='Add' toggle={this.toggleAdd} isSelected={this.state.queryAction === 'add'} disabled={this.state.isDisabled} />
+                <ToggleButton icon='minus' label='Remove' toggle={this.toggleRemove} isSelected={this.state.queryAction === 'remove'} disabled={this.state.isDisabled} />
+                <ToggleButton icon='flash' label='Set' toggle={this.toggleOnly} isSelected={this.state.queryAction === 'set'} disabled={this.state.isDisabled} />
             </div>
             {this.state.queryAction && <ActionMenu items={this.queries} onSelect={this.selectQuery} />}
         </div>
@@ -165,20 +165,20 @@ export class StructureSelectionControls<P, S extends StructureSelectionControlsS
             </li>)
         }
 
-        return <div>
+        return <>
+            {this.controls}
+            <ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
             <div className='msp-control-row msp-row-text'>
                 <button className='msp-btn msp-btn-block' onClick={this.focus}>
                     <Icon name='focus-on-visual' style={{ position: 'absolute', left: '5px' }} />
                     {this.stats}
                 </button>
             </div>
-            <ParameterControls params={StructureSelectionParams} values={this.values} onChange={this.setProps} isDisabled={this.state.isDisabled} />
-            {this.controls}
             {history.length > 0 && <ExpandGroup header='Selection History'>
                 <ul style={{ listStyle: 'none', marginTop: '1px', marginBottom: '0' }} className='msp-state-list'>
                     {history}
                 </ul>
             </ExpandGroup>}
-        </div>
+        </>
     }
 }

+ 0 - 1
src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts

@@ -47,7 +47,6 @@ export enum StructureRepresentationInteractionTags {
 const TagSet: Set<StructureRepresentationInteractionTags> = new Set([StructureRepresentationInteractionTags.Group, StructureRepresentationInteractionTags.ResidueSel, StructureRepresentationInteractionTags.ResidueRepr, StructureRepresentationInteractionTags.SurrSel, StructureRepresentationInteractionTags.SurrRepr, StructureRepresentationInteractionTags.SurrNciRepr])
 
 export class StructureRepresentationInteractionBehavior extends PluginBehavior.WithSubscribers<StructureRepresentationInteractionProps> {
-
     private createResVisualParams(s: Structure) {
         return StructureRepresentation3DHelpers.createParams(this.plugin, s, {
             repr: [BuiltInStructureRepresentations['ball-and-stick'], () => ({ })],

+ 4 - 4
src/mol-plugin/context.ts

@@ -41,7 +41,6 @@ import { StructureOverpaintHelper } from './util/structure-overpaint-helper';
 import { PluginToastManager } from './util/toast';
 import { StructureMeasurementManager } from '../mol-plugin-state/manager/structure/measurement';
 import { ViewportScreenshotHelper } from './util/viewport-screenshot';
-import { RepresentationBuilder } from '../mol-plugin-state/builder/representation';
 import { CustomProperty } from '../mol-model-props/common/custom-property';
 import { PluginConfigManager } from './config';
 import { DataBuilder } from '../mol-plugin-state/builder/data';
@@ -49,6 +48,7 @@ import { StructureBuilder } from '../mol-plugin-state/builder/structure';
 import { StructureHierarchyManager } from '../mol-plugin-state/manager/structure/hierarchy';
 import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
 import { TrajectoryFormatRegistry } from '../mol-plugin-state/formats/trajectory';
+import { StructureComponentManager } from '../mol-plugin-state/manager/structure/component';
 
 export class PluginContext {
     private disposed = false;
@@ -124,13 +124,13 @@ export class PluginContext {
 
     readonly builders = {
         data: new DataBuilder(this),
-        structure: new StructureBuilder(this),
-        representation: void 0 as any as RepresentationBuilder
+        structure: void 0 as any as StructureBuilder
     };
 
     readonly managers = {
         structure: {
             hierarchy: new StructureHierarchyManager(this),
+            component: new StructureComponentManager(this),
             measurement: new StructureMeasurementManager(this),
             selection: new StructureSelectionManager(this)
         },
@@ -298,7 +298,7 @@ export class PluginContext {
         (this.managers.interactivity as InteractivityManager) = new InteractivityManager(this);
         (this.managers.lociLabels as LociLabelManager) = new LociLabelManager(this);
 
-        (this.builders.representation as RepresentationBuilder)= new RepresentationBuilder(this);
+        (this.builders.structure as StructureBuilder) = new StructureBuilder(this);
 
         this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`);
         if (!isProductionMode) this.log.message(`Development mode enabled`);