Browse Source

wip, focus manager, focus repr refactoring

Alexander Rose 5 years ago
parent
commit
b1da60e1c0

+ 3 - 3
src/mol-model/structure/structure/element/location.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -21,9 +21,9 @@ interface Location<U = Unit> {
 }
 
 namespace Location {
-    export function create<U extends Unit>(structure: Structure | undefined, unit?: U, element?: ElementIndex): Location<U> {
+    export function create<U extends Unit>(structure?: Structure, unit?: U, element?: ElementIndex): Location<U> {
         return {
-            kind: 'element-location', 
+            kind: 'element-location',
             structure: structure as any,
             unit: unit as any,
             element: element || (0 as ElementIndex)

+ 115 - 0
src/mol-plugin-state/manager/structure/focus.ts

@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StatefulPluginComponent } from '../../component';
+import { PluginContext } from '../../../mol-plugin/context';
+import { arrayRemoveAtInPlace } from '../../../mol-util/array';
+import { StructureElement, Bond, Structure } from '../../../mol-model/structure';
+import { Loci } from '../../../mol-model/loci';
+import { lociLabel } from '../../../mol-theme/label';
+
+export type FocusEntry = { label: string, loci: StructureElement.Loci, category?: string }
+
+interface StructureFocusManagerState {
+    current?: FocusEntry,
+    history: FocusEntry[],
+}
+
+const HISTORY_CAPACITY = 12;
+
+export class StructureFocusManager extends StatefulPluginComponent<StructureFocusManagerState> {
+
+    readonly events = {
+        changed: this.ev<undefined>(),
+        historyUpdated: this.ev<undefined>()
+    }
+
+    get current() { return this.state.current; }
+    get history() { return this.state.history; }
+
+    private tryAddHistory(entry: FocusEntry) {
+        if (StructureElement.Loci.isEmpty(entry.loci)) return;
+
+        let idx = 0, existingEntry: FocusEntry | undefined = void 0;
+        for (const e of this.state.history) {
+            if (StructureElement.Loci.areEqual(e.loci, entry.loci)) {
+                existingEntry = e;
+                break;
+            }
+            idx++;
+        }
+
+        if (existingEntry) {
+            // move to top, use new
+            arrayRemoveAtInPlace(this.state.history, idx);
+            this.state.history.unshift(entry);
+            this.events.historyUpdated.next();
+            return;
+        }
+
+        this.state.history.unshift(entry);
+        if (this.state.history.length > HISTORY_CAPACITY) this.state.history.pop();
+
+        this.events.historyUpdated.next();
+    }
+
+    set(entry: FocusEntry) {
+        this.tryAddHistory(entry)
+        if (!this.state.current || !StructureElement.Loci.areEqual(this.state.current.loci, entry.loci)) {
+            this.state.current = entry
+            this.events.changed.next()
+        }
+    }
+
+    setFromLoci(anyLoci: Loci) {
+        let loci: StructureElement.Loci;
+        if (StructureElement.Loci.is(anyLoci)) {
+            loci = anyLoci;
+        } else if (Bond.isLoci(anyLoci)) {
+            loci = Bond.toStructureElementLoci(anyLoci);
+        } else if (Structure.isLoci(anyLoci)) {
+            loci = Structure.toStructureElementLoci(anyLoci.structure);
+        } else {
+            this.clear()
+            return
+        }
+
+        if (StructureElement.Loci.isEmpty(loci)) {
+            this.clear()
+            return
+        }
+
+        this.set({ loci, label: lociLabel(loci, { reverse: true, hidePrefix: true, htmlStyling: false }) })
+    }
+
+    clear() {
+        if (this.state.current) {
+            this.state.current = undefined
+            this.events.changed.next()
+        }
+    }
+
+    // this.subscribeObservable(this.plugin.events.state.object.removed, o => {
+    //     if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(lastLoci)) return;
+    //     if (lastLoci.structure === o.obj.data) {
+    //         lastLoci = EmptyLoci;
+    //     }
+    // });
+
+    // this.subscribeObservable(this.plugin.events.state.object.updated, o => {
+    //     if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(lastLoci)) return;
+    //     if (lastLoci.structure === o.oldObj.data) {
+    //         lastLoci = EmptyLoci;
+    //     }
+    // });
+
+    constructor(plugin: PluginContext) {
+        super({ history: [] });
+
+        // plugin.state.data.events.object.removed.subscribe(e => this.onRemove(e.ref));
+        // plugin.state.data.events.object.updated.subscribe(e => this.onUpdate(e.ref, e.oldObj, e.obj));
+    }
+}

+ 1 - 17
src/mol-plugin-state/manager/structure/selection.ts

@@ -173,6 +173,7 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
         }
 
         if (entry) {
+            // move to top
             arrayRemoveAtInPlace(this.additionsHistory, idx);
             this.additionsHistory.unshift(entry);
             this.events.additionsHistoryUpdated.next();
@@ -188,23 +189,6 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure
         this.events.additionsHistoryUpdated.next();
     }
 
-    // private removeHistory(loci: Loci) {
-    //     if (Loci.isEmpty(loci)) return;
-
-    //     let idx = 0, found = false;
-    //     for (const l of this.history) {
-    //         if (Loci.areEqual(l.loci, loci)) {
-    //             found = true;
-    //             break;
-    //         }
-    //         idx++;
-    //     }
-
-    //     if (found) {
-    //         arrayRemoveAtInPlace(this.history, idx);
-    //     }
-    // }
-
     private onRemove(ref: string) {
         if (this.entries.has(ref)) {
             this.entries.delete(ref);

+ 2 - 0
src/mol-plugin-ui/controls.tsx

@@ -22,6 +22,7 @@ import { Icon } from './controls/icons';
 import { StructureComponentControls } from './structure/components';
 import { StructureSourceControls } from './structure/source';
 import { VolumeStreamingControls } from './structure/volume';
+import { StructureFocusControls } from './structure/focus';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -287,6 +288,7 @@ export class DefaultStructureTools extends PluginUIComponent {
             <StructureSelectionControls />
             <StructureMeasurementsControls />
             <StructureComponentControls />
+            <StructureFocusControls />
             <VolumeStreamingControls />
 
             <CustomStructureControls />

+ 139 - 0
src/mol-plugin-ui/structure/focus.tsx

@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react';
+import { CollapsableState, CollapsableControls } from '../base';
+import { ToggleButton } from '../controls/common';
+import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
+import { ActionMenu } from '../controls/action-menu';
+import { stringToWords } from '../../mol-util/string';
+import { StructureElement, StructureProperties } from '../../mol-model/structure';
+import { OrderedSet, SortedArray } from '../../mol-data/int';
+import { UnitIndex } from '../../mol-model/structure/structure/element/element';
+import { FocusEntry } from '../../mol-plugin-state/manager/structure/focus';
+import { Icon } from '../controls/icons';
+
+type FocusAction = 'presets' | 'history'
+
+interface StructureComponentControlState extends CollapsableState {
+    isBusy: boolean
+    action?: FocusAction
+}
+
+export class StructureFocusControls extends CollapsableControls<{}, StructureComponentControlState> {
+    protected defaultState(): StructureComponentControlState {
+        return {
+            header: 'Focus',
+            isCollapsed: false,
+            isBusy: false,
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.structure.hierarchy.behaviors.selection, c => {
+            this.setState({
+                description: StructureHierarchyManager.getSelectedStructuresDescription(this.plugin)
+            })
+            this.forceUpdate();
+        });
+    }
+
+    get presetsItems() {
+        const items: FocusEntry[] = []
+        const l = StructureElement.Location.create()
+        const { structures } = this.plugin.managers.structure.hierarchy.selection;
+        for (const s of structures) {
+            const d = s.cell.obj?.data
+            if (d) {
+                l.structure = d
+                for (const ug of d.unitSymmetryGroups) {
+                    l.unit = ug.units[0]
+                    l.element = ug.elements[0]
+                    const et = StructureProperties.entity.type(l)
+                    if (et === 'non-polymer') {
+                        const idx = SortedArray.indexOf(ug.elements, l.element) as UnitIndex
+                        const loci = StructureElement.Loci(d, [{ unit: l.unit, indices: OrderedSet.ofSingleton(idx) }])
+                        items.push({
+                            label: StructureProperties.entity.pdbx_description(l).join(', '),
+                            loci: StructureElement.Loci.extendToWholeResidues(loci)
+                        })
+                    }
+                }
+            }
+        }
+
+        return items
+    }
+
+    get historyItems() {
+        return this.plugin.managers.structure.focus.history
+    }
+
+    get actionItems() {
+        let items: FocusEntry[]
+        switch (this.state.action) {
+            case 'presets': items = this.presetsItems; break
+            case 'history': items = this.historyItems; break
+            default: items = []
+        }
+        return ActionMenu.createItems(items, {
+            label: f => f.label,
+            category: f => f.category
+        })
+    }
+
+    selectAction: ActionMenu.OnSelect = item => {
+        if (!item || !this.state.action) {
+            this.setState({ action: void 0 });
+            return;
+        }
+        this.setState({ action: void 0 }, async () => {
+            const f = item.value as FocusEntry
+            this.plugin.managers.structure.focus.set(f)
+            this.plugin.managers.camera.focusLoci(f.loci, { durationMs: 0 })
+        })
+    }
+
+    private showAction(a: FocusAction) {
+        return () => this.setState({ action: this.state.action === a ? void 0 : a });
+    }
+
+    togglePresets = this.showAction('presets')
+    toggleHistory = this.showAction('history')
+
+    focus = () => {
+        const { current } = this.plugin.managers.structure.focus
+        if (current) this.plugin.managers.camera.focusLoci(current.loci);
+    }
+
+    highlightCurrent = () => {
+        const { current } = this.plugin.managers.structure.focus
+        if (current) this.plugin.managers.interactivity.lociHighlights.highlightOnly({ loci: current.loci }, false);
+    }
+
+    clearHighlights = () => {
+        this.plugin.managers.interactivity.lociHighlights.clearHighlights()
+    }
+
+    renderControls() {
+        const { current } = this.plugin.managers.structure.focus
+        const label = current?.label || 'Nothing Focused'
+
+        return <>
+            <div className='msp-control-row msp-select-row'>
+                <ToggleButton icon='bookmarks' title='Preset' label='Preset' toggle={this.togglePresets} isSelected={this.state.action === 'presets'} disabled={this.state.isBusy} />
+                <ToggleButton icon='clock' title='History' label='History' toggle={this.toggleHistory} isSelected={this.state.action === 'history'} disabled={this.state.isBusy} />
+            </div>
+            {this.state.action && <ActionMenu header={stringToWords(this.state.action)} items={this.actionItems} onSelect={this.selectAction} />}
+            <div className='msp-control-row msp-row-text' style={{ marginTop: '6px' }}>
+                <button className='msp-btn msp-btn-block msp-no-overflow' onClick={this.focus} title='Click to Center Focused' disabled={!current} onMouseEnter={this.highlightCurrent} onMouseLeave={this.clearHighlights}>
+                    <Icon name='focus-on-visual' style={{ position: 'absolute', left: '5px' }} />
+                    {label}
+                </button>
+            </div>
+        </>;
+    }
+}

+ 2 - 1
src/mol-plugin-ui/structure/volume.tsx

@@ -12,6 +12,7 @@ import { UpdateTransformControl } from '../state/update-transform';
 import { BindingsHelp } from '../viewport/help';
 import { ExpandGroup } from '../controls/common';
 import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
+import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 
 interface VolumeStreamingControlState extends CollapsableState {
     isBusy: boolean
@@ -56,7 +57,7 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
 
     renderParams() {
         const pivot = this.pivot;
-        const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && pivot.volumeStreaming?.cell.transform.params?.bindings;
+        const bindings = pivot.volumeStreaming?.cell.transform.params?.entry.params.view.name === 'selection-box' && this.plugin.state.behaviors.cells.get(FocusLoci.id)?.params?.values?.bindings;
         return <>
             <UpdateTransformControl state={pivot.cell.parent} transform={pivot.volumeStreaming!.cell.transform} customHeader='none' noMargin autoHideApply />
             {bindings && <ExpandGroup header='Controls Help'>

+ 5 - 20
src/mol-plugin-ui/viewport/help.tsx

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,7 +9,7 @@ import { Binding } from '../../mol-util/binding';
 import { PluginUIComponent } from '../base';
 import { StateTransformer, StateSelection } from '../../mol-state';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
-import { StructureRepresentationInteraction } from '../../mol-plugin/behavior/dynamic/selection/structure-representation-interaction';
+import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { Icon } from '../controls/icons';
 
 function getBindingsList(bindings: { [k: string]: Binding }) {
@@ -25,7 +25,7 @@ export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]:
                 return !Binding.isEmpty(binding)
                     ? <div key={name} style={{ marginBottom: '6px' }}>
                         <b>{binding.action}</b><br /><span dangerouslySetInnerHTML={{ __html: Binding.format(binding, name) }} />
-                    </div> 
+                    </div>
                     : null
             })}
         </>
@@ -113,8 +113,7 @@ export class HelpContent extends PluginUIComponent {
 
     render() {
         const selectToggleTriggers = this.getTriggerFor(SelectLoci, 'clickSelectToggle')
-        const structureInteractionTriggers = this.getTriggerFor(StructureRepresentationInteraction, 'clickInteractionAroundOnly')
-        // const volumeAroundTriggers = this.getTriggerFor(StructureRepresentationInteraction, 'clickInteractionAroundOnly') // TODO get from correct behavior transform
+        const focusTriggers = this.getTriggerFor(FocusLoci, 'clickFocus')
 
         // TODO: interactive help, for example for density
 
@@ -145,25 +144,11 @@ export class HelpContent extends PluginUIComponent {
             </HelpGroup>
             <HelpGroup header='Surroundings'>
                 <HelpText>
-                    To show the surroundings of a residue or ligand, click it in the 3D scene or in the sequence widget using {structureInteractionTriggers}.
+                    To show the surroundings of a residue or ligand, click it in the 3D scene or in the sequence widget using {focusTriggers}.
                 </HelpText>
             </HelpGroup>
-            {/* <HelpGroup header='Densities'>
-                <HelpText>
-                    Densities can be shown for both X-ray and cryo-EM structures. By default the density around an element/atom can be shown by clicking using {volumeAroundTriggers}. The <i>Density Controls</i> panel offers a variety of options to adjust the display of density maps. The absence of the <i>Density Controls</i> panel indicates that no density is available for the loaded entry which is the case for e.g. NMR structures or very old X-ray structures.
-                </HelpText>
-            </HelpGroup> */}
 
             <HelpSection header='How-to Guides' />
-            {/* <HelpGroup header='RCSB Molecule of the Month Style'>
-                <HelpText>
-                    <ol style={{ paddingLeft: '20px' }}>
-                        <li>First, hide everything, then show everything with the spacefill representation using the <i>Representation</i> panel.</li>
-                        <li>Change color theme of the spacefill representation to <i>illustrative</i> using the <i>Structure Settings</i> panel.</li>
-                        <li>Set render style to <i>toon</i> and activate <i>occlusion</i> in the <i>General Settings</i> panel.</li>
-                    </ol>
-                </HelpText>
-            </HelpGroup> */}
             <HelpGroup header='Create an Image'>
                 <HelpText>
                     <p>Use the <Icon name='screenshot' /> icon in the viewport to bring up the screenshot controls.</p>

+ 4 - 9
src/mol-plugin/behavior/dynamic/camera.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -20,7 +20,7 @@ const DefaultFocusLociBindings = {
     clickCenterFocus: Binding([
         Trigger(B.Flag.Auxilary, M.create()),
         Trigger(B.Flag.Primary, M.create({ alt: true }))
-    ], 'Center and focus', 'Click element using ${triggers}'),
+    ], 'Camera center and focus', 'Click element using ${triggers}'),
 }
 const FocusLociParams = {
     minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
@@ -38,22 +38,17 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({
         register(): void {
             this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
                 if (!this.ctx.canvas3d) return;
-                const p = this.params;
                 if (Binding.match(this.params.bindings.clickCenterFocus, button, modifiers)) {
                     const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity)
                     if (Loci.isEmpty(loci)) {
                         PluginCommands.Camera.Reset(this.ctx, { })
                     } else {
-                        const sphere = Loci.getBoundingSphere(loci);
-                        if (sphere) {
-                            const radius = Math.max(sphere.radius + p.extraRadius, p.minRadius);
-                            this.ctx.canvas3d.camera.focus(sphere.center, radius, p.durationMs);
-                        }
+                        this.ctx.managers.camera.focusLoci(loci, this.params)
                     }
                 }
             });
         }
     },
     params: () => FocusLociParams,
-    display: { name: 'Focus Loci on Canvas' }
+    display: { name: 'Camera Focus Loci on Canvas' }
 });

+ 38 - 0
src/mol-plugin/behavior/dynamic/representation.ts

@@ -167,6 +167,8 @@ export const SelectLoci = PluginBehavior.create({
     display: { name: 'Select Loci on Canvas' }
 });
 
+//
+
 export const DefaultLociLabelProvider = PluginBehavior.create({
     name: 'default-loci-label-provider',
     category: 'interaction',
@@ -178,3 +180,39 @@ export const DefaultLociLabelProvider = PluginBehavior.create({
     },
     display: { name: 'Provide Default Loci Label' }
 });
+
+//
+
+const DefaultFocusLociBindings = {
+    clickFocus: Binding([
+        Trigger(B.Flag.Secondary, M.create()),
+        Trigger(B.Flag.Primary, M.create({ control: true }))
+    ], 'Representation Focus', 'Click element using ${triggers}'),
+}
+const FocusLociParams = {
+    bindings: PD.Value(DefaultFocusLociBindings, { isHidden: true }),
+}
+type FocusLociProps = PD.Values<typeof FocusLociParams>
+
+export const FocusLoci = PluginBehavior.create<FocusLociProps>({
+    name: 'representation-focus-loci',
+    category: 'interaction',
+    ctor: class extends PluginBehavior.Handler<FocusLociProps> {
+        register(): void {
+            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
+                const { clickFocus } = this.params.bindings
+
+                if (Binding.match(clickFocus, button, modifiers)) {
+                    const entry = this.ctx.managers.structure.focus.current
+                    if (entry && Loci.areEqual(entry.loci, current.loci)) {
+                        this.ctx.managers.structure.focus.clear()
+                    } else {
+                        this.ctx.managers.structure.focus.setFromLoci(Loci.applyGranularity(current.loci, 'residue'))
+                    }
+                }
+            });
+        }
+    },
+    params: () => FocusLociParams,
+    display: { name: 'Representation Focus Loci on Canvas' }
+});

+ 186 - 0
src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts

@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { InteractionsRepresentationProvider } from '../../../../mol-model-props/computed/representations/interactions';
+import { InteractionTypeColorThemeProvider } from '../../../../mol-model-props/computed/themes/interaction-type';
+import { StructureElement } from '../../../../mol-model/structure';
+import { createStructureRepresentationParams } from '../../../../mol-plugin-state/helpers/structure-representation-params';
+import { PluginStateObject } from '../../../../mol-plugin-state/objects';
+import { StateTransforms } from '../../../../mol-plugin-state/transforms';
+import { PluginBehavior } from '../../../behavior';
+import { MolScriptBuilder as MS } from '../../../../mol-script/language/builder';
+import { StateObjectCell, StateSelection, StateTransform } from '../../../../mol-state';
+import { SizeTheme } from '../../../../mol-theme/size';
+import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
+import { PluginCommands } from '../../../commands';
+import { PluginContext } from '../../../context';
+
+const StructureFocusRepresentationParams = (plugin: PluginContext) => {
+    const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
+    return {
+        // TODO: min = 0 to turn them off?
+        expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
+        targetParams: PD.Group(reprParams, {
+            label: 'Target',
+            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'uniform' })
+        }),
+        surroundingsParams: PD.Group(reprParams, {
+            label: 'Surroundings',
+            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', color: 'element-symbol', size: 'uniform' })
+        }),
+        nciParams: PD.Group(reprParams, {
+            label: 'Non-covalent Int.',
+            customDefault: createStructureRepresentationParams(plugin, void 0, {
+                type: InteractionsRepresentationProvider,
+                color: InteractionTypeColorThemeProvider,
+                size: SizeTheme.BuiltIn.uniform
+            })
+        })
+    };
+}
+
+type StructureFocusRepresentationProps = PD.ValuesFor<ReturnType<typeof StructureFocusRepresentationParams>>
+
+export enum StructureFocusRepresentationTags {
+    TargetSel = 'structure-focus-target-sel',
+    TargetRepr = 'structure-focus-target-repr',
+    SurrSel = 'structure-focus-surr-sel',
+    SurrRepr = 'structure-focus-surr-repr',
+    SurrNciRepr = 'structure-focus-surr-nci-repr'
+}
+
+const TagSet: Set<StructureFocusRepresentationTags> = new Set([StructureFocusRepresentationTags.TargetSel, StructureFocusRepresentationTags.TargetRepr, StructureFocusRepresentationTags.SurrSel, StructureFocusRepresentationTags.SurrRepr, StructureFocusRepresentationTags.SurrNciRepr])
+
+export class StructureFocusRepresentationBehavior extends PluginBehavior.WithSubscribers<StructureFocusRepresentationProps> {
+    private get surrLabel() { return `[Focus] Surroundings (${this.params.expandRadius} Å)`; }
+
+    private ensureShape(cell: StateObjectCell<PluginStateObject.Molecule.Structure>) {
+        const state = this.plugin.state.data, tree = state.tree;
+        const builder = state.build();
+        const refs = StateSelection.findUniqueTagsInSubtree(tree, cell.transform.ref, TagSet);
+
+        // Selections
+        if (!refs[StructureFocusRepresentationTags.TargetSel]) {
+            refs[StructureFocusRepresentationTags.TargetSel] = builder
+                .to(cell)
+                .apply(StateTransforms.Model.StructureSelectionFromBundle,
+                    { bundle: StructureElement.Bundle.Empty, label: '[Focus] Target' }, { tags: StructureFocusRepresentationTags.TargetSel }).ref;
+        }
+
+        if (!refs[StructureFocusRepresentationTags.SurrSel]) {
+            refs[StructureFocusRepresentationTags.SurrSel] = builder
+                .to(cell)
+                .apply(StateTransforms.Model.StructureSelectionFromExpression,
+                    { expression: MS.struct.generator.empty(), label: this.surrLabel }, { tags: StructureFocusRepresentationTags.SurrSel }).ref;
+        }
+
+        // Representations
+        if (!refs[StructureFocusRepresentationTags.TargetRepr]) {
+            refs[StructureFocusRepresentationTags.TargetRepr] = builder
+                .to(refs[StructureFocusRepresentationTags.TargetSel]!)
+                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.targetParams, { tags: StructureFocusRepresentationTags.TargetRepr }).ref;
+        }
+
+        if (!refs[StructureFocusRepresentationTags.SurrRepr]) {
+            refs[StructureFocusRepresentationTags.SurrRepr] = builder
+                .to(refs[StructureFocusRepresentationTags.SurrSel]!)
+                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.surroundingsParams, { tags: StructureFocusRepresentationTags.SurrRepr }).ref;
+        }
+
+        if (!refs[StructureFocusRepresentationTags.SurrNciRepr]) {
+            refs[StructureFocusRepresentationTags.SurrNciRepr] = builder
+                .to(refs[StructureFocusRepresentationTags.SurrSel]!)
+                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.nciParams, { tags: StructureFocusRepresentationTags.SurrNciRepr }).ref;
+        }
+
+        return { state, builder, refs };
+    }
+
+    private clear(root: StateTransform.Ref) {
+        const state = this.plugin.state.data;
+
+        const foci = state.select(StateSelection.Generators.byRef(root).subtree().withTag(StructureFocusRepresentationTags.TargetSel));
+        const surrs = state.select(StateSelection.Generators.byRef(root).subtree().withTag(StructureFocusRepresentationTags.SurrSel));
+        if (foci.length === 0 && surrs.length === 0) return;
+
+        const update = state.build();
+        const bundle = StructureElement.Bundle.Empty;
+        for (const f of foci) {
+            update.to(f).update(StateTransforms.Model.StructureSelectionFromBundle, old => ({ ...old, bundle }));
+        }
+
+        const expression = MS.struct.generator.empty();
+        for (const s of surrs) {
+            update.to(s).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression }));
+        }
+
+        return PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+    }
+
+    private focus(loci: StructureElement.Loci) {
+        const parent = this.plugin.helpers.substructureParent.get(loci.structure);
+        if (!parent || !parent.obj) return;
+
+        const residueLoci = StructureElement.Loci.extendToWholeResidues(StructureElement.Loci.remap(loci, parent.obj!.data))
+        const residueBundle = StructureElement.Bundle.fromLoci(residueLoci)
+
+        const surroundings = MS.struct.modifier.includeSurroundings({
+            0: StructureElement.Bundle.toExpression(residueBundle),
+            radius: this.params.expandRadius,
+            'as-whole-residues': true
+        });
+
+        const { state, builder, refs } = this.ensureShape(parent);
+
+        builder.to(refs[StructureFocusRepresentationTags.TargetSel]!).update(StateTransforms.Model.StructureSelectionFromBundle, old => ({ ...old, bundle: residueBundle }));
+        builder.to(refs[StructureFocusRepresentationTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings, label: this.surrLabel }));
+
+        PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+    }
+
+    register(ref: string): void {
+        this.subscribeObservable(this.plugin.managers.structure.focus.events.changed, () => {
+            const entry = this.plugin.managers.structure.focus.current
+            if (entry) this.focus(entry.loci)
+            else this.clear(StateTransform.RootRef)
+        });
+    }
+
+    async update(params: StructureFocusRepresentationProps) {
+        let oldRadius = this.params.expandRadius;
+        this.params = params;
+
+        const state = this.plugin.state.data;
+        const builder = state.build();
+
+        const all = StateSelection.Generators.root.subtree();
+        for (const repr of state.select(all.withTag(StructureFocusRepresentationTags.TargetRepr))) {
+            builder.to(repr).update(this.params.targetParams);
+        }
+        for (const repr of state.select(all.withTag(StructureFocusRepresentationTags.SurrRepr))) {
+            builder.to(repr).update(this.params.surroundingsParams);
+        }
+        for (const repr of state.select(all.withTag(StructureFocusRepresentationTags.SurrNciRepr))) {
+            builder.to(repr).update(this.params.nciParams);
+        }
+
+        await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
+
+        // TODO: update properly
+        if (params.expandRadius !== oldRadius) await this.clear(StateTransform.RootRef);
+
+        return true;
+    }
+}
+
+export const StructureFocusRepresentation = PluginBehavior.create({
+    name: 'create-structure-focus-representation',
+    display: { name: 'Structure Focus Representation' },
+    category: 'interaction',
+    ctor: StructureFocusRepresentationBehavior,
+    params: (_, plugin) => StructureFocusRepresentationParams(plugin)
+});

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

@@ -1,239 +0,0 @@
-/**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { InteractionsRepresentationProvider } from '../../../../mol-model-props/computed/representations/interactions';
-import { InteractionTypeColorThemeProvider } from '../../../../mol-model-props/computed/themes/interaction-type';
-import { EmptyLoci, isEmptyLoci, Loci } from '../../../../mol-model/loci';
-import { Bond, Structure, StructureElement } from '../../../../mol-model/structure';
-import { createStructureRepresentationParams } from '../../../../mol-plugin-state/helpers/structure-representation-params';
-import { PluginStateObject } from '../../../../mol-plugin-state/objects';
-import { StateTransforms } from '../../../../mol-plugin-state/transforms';
-import { PluginBehavior } from '../../../../mol-plugin/behavior';
-import { MolScriptBuilder as MS } from '../../../../mol-script/language/builder';
-import { StateObjectCell, StateSelection, StateTransform } from '../../../../mol-state';
-import { SizeTheme } from '../../../../mol-theme/size';
-import { Binding } from '../../../../mol-util/binding';
-import { ButtonsType, ModifiersKeys } from '../../../../mol-util/input/input-observer';
-import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
-import { PluginCommands } from '../../../commands';
-import { PluginContext } from '../../../context';
-
-const B = ButtonsType
-const M = ModifiersKeys
-const Trigger = Binding.Trigger
-
-const DefaultStructureRepresentationInteractionBindings = {
-    clickInteractionAroundOnly: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Structure element interaction', 'Click element using ${triggers}; on nothing/same element to hide'),
-}
-
-const StructureRepresentationInteractionParams = (plugin: PluginContext) => {
-    const reprParams = StateTransforms.Representation.StructureRepresentation3D.definition.params!(void 0, plugin) as PD.Params;
-    return {
-        bindings: PD.Value(DefaultStructureRepresentationInteractionBindings, { isHidden: true }),
-        // TODO: min = 0 to turn them off?
-        expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }),
-        focusParams: PD.Group(reprParams, {
-            label: 'Focus',
-            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'uniform' })
-        }),
-        surroundingsParams: PD.Group(reprParams, {
-            label: 'Surroundings',
-            customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', color: 'element-symbol', size: 'uniform' })
-        }),
-        nciParams: PD.Group(reprParams, {
-            label: 'Non-covalent Int.',
-            customDefault: createStructureRepresentationParams(plugin, void 0, {
-                type: InteractionsRepresentationProvider,
-                color: InteractionTypeColorThemeProvider,
-                size: SizeTheme.BuiltIn.uniform
-            })
-        })
-    };
-}
-
-type StructureRepresentationInteractionProps = PD.ValuesFor<ReturnType<typeof StructureRepresentationInteractionParams>>
-
-export enum StructureRepresentationInteractionTags {
-    ResidueSel = 'structure-interaction-residue-sel',
-    ResidueRepr = 'structure-interaction-residue-repr',
-    SurrSel = 'structure-interaction-surr-sel',
-    SurrRepr = 'structure-interaction-surr-repr',
-    SurrNciRepr = 'structure-interaction-surr-nci-repr'
-}
-
-const TagSet: Set<StructureRepresentationInteractionTags> = new Set([StructureRepresentationInteractionTags.ResidueSel, StructureRepresentationInteractionTags.ResidueRepr, StructureRepresentationInteractionTags.SurrSel, StructureRepresentationInteractionTags.SurrRepr, StructureRepresentationInteractionTags.SurrNciRepr])
-
-export class StructureRepresentationInteractionBehavior extends PluginBehavior.WithSubscribers<StructureRepresentationInteractionProps> {
-    private get surrLabel() { return `[Focus +${this.params.expandRadius} Å Surrounding]`; }
-
-    private ensureShape(cell: StateObjectCell<PluginStateObject.Molecule.Structure>) {
-        const state = this.plugin.state.data, tree = state.tree;
-        const builder = state.build();
-        const refs = StateSelection.findUniqueTagsInSubtree(tree, cell.transform.ref, TagSet);
-
-        // Selections
-        if (!refs[StructureRepresentationInteractionTags.ResidueSel]) {
-            refs[StructureRepresentationInteractionTags.ResidueSel] = builder
-                .to(cell) // refs['structure-interaction-group'])
-                .apply(StateTransforms.Model.StructureSelectionFromBundle,
-                    { bundle: StructureElement.Bundle.Empty, label: '[Focus]' }, { tags: StructureRepresentationInteractionTags.ResidueSel }).ref;
-        }
-
-        if (!refs[StructureRepresentationInteractionTags.SurrSel]) {
-            refs[StructureRepresentationInteractionTags.SurrSel] = builder
-                .to(cell) // .to(refs['structure-interaction-group'])
-                .apply(StateTransforms.Model.StructureSelectionFromExpression,
-                    { expression: MS.struct.generator.empty(), label: this.surrLabel }, { tags: StructureRepresentationInteractionTags.SurrSel }).ref;
-        }
-
-        // Representations
-        if (!refs[StructureRepresentationInteractionTags.ResidueRepr]) {
-            refs[StructureRepresentationInteractionTags.ResidueRepr] = builder
-                .to(refs['structure-interaction-residue-sel']!)
-                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.focusParams, { tags: StructureRepresentationInteractionTags.ResidueRepr }).ref;
-        }
-
-        if (!refs[StructureRepresentationInteractionTags.SurrRepr]) {
-            refs[StructureRepresentationInteractionTags.SurrRepr] = builder
-                .to(refs['structure-interaction-surr-sel']!)
-                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.surroundingsParams, { tags: StructureRepresentationInteractionTags.SurrRepr }).ref;
-        }
-
-        if (!refs[StructureRepresentationInteractionTags.SurrNciRepr]) {
-            refs[StructureRepresentationInteractionTags.SurrNciRepr] = builder
-                .to(refs['structure-interaction-surr-sel']!)
-                .apply(StateTransforms.Representation.StructureRepresentation3D, this.params.nciParams, { tags: StructureRepresentationInteractionTags.SurrNciRepr }).ref;
-        }
-
-        return { state, builder, refs };
-    }
-
-    private clear(root: StateTransform.Ref) {
-        const state = this.plugin.state.data;
-
-        const foci = state.select(StateSelection.Generators.byRef(root).subtree().withTag(StructureRepresentationInteractionTags.ResidueSel));
-        const surrs = state.select(StateSelection.Generators.byRef(root).subtree().withTag(StructureRepresentationInteractionTags.SurrSel));
-        if (foci.length === 0 && surrs.length === 0) return;
-
-        const update = state.build();
-        const bundle = StructureElement.Bundle.Empty;
-        for (const f of foci) {
-            update.to(f).update(StateTransforms.Model.StructureSelectionFromBundle, old => ({ ...old, bundle }));
-        }
-
-        const expression = MS.struct.generator.empty();
-        for (const s of surrs) {
-            update.to(s).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression }));
-        }
-
-        return PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
-    }
-
-    register(ref: string): void {
-        let lastLoci: Loci = EmptyLoci;
-
-        this.subscribeObservable(this.plugin.events.state.object.removed, o => {
-            if (!PluginStateObject.Molecule.Structure.is(o.obj) || !StructureElement.Loci.is(lastLoci)) return;
-            if (lastLoci.structure === o.obj.data) {
-                lastLoci = EmptyLoci;
-            }
-        });
-
-        this.subscribeObservable(this.plugin.events.state.object.updated, o => {
-            if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || !StructureElement.Loci.is(lastLoci)) return;
-            if (lastLoci.structure === o.oldObj.data) {
-                lastLoci = EmptyLoci;
-            }
-        });
-
-        this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, button, modifiers }) => {
-            const { clickInteractionAroundOnly } = this.params.bindings
-
-            if (Binding.match(clickInteractionAroundOnly, button, modifiers)) {
-                if (isEmptyLoci(current.loci)) {
-                    this.clear(StateTransform.RootRef);
-                    lastLoci = current.loci;
-                    return;
-                }
-
-                let loci: StructureElement.Loci;
-                if (StructureElement.Loci.is(current.loci)) {
-                    loci = current.loci;
-                } else if (Bond.isLoci(current.loci)) {
-                    loci = Bond.toStructureElementLoci(current.loci);
-                } else if (Structure.isLoci(current.loci)) {
-                    loci = Structure.toStructureElementLoci(current.loci.structure);
-                } else {
-                    return;
-                }
-
-                if (StructureElement.Loci.isEmpty(loci)) return;
-
-                const parent = this.plugin.helpers.substructureParent.get(loci.structure);
-                if (!parent || !parent.obj) return;
-
-                if (Loci.areEqual(lastLoci, loci)) {
-                    lastLoci = EmptyLoci;
-                    this.clear(parent.transform.ref);
-                    return;
-                }
-
-                lastLoci = loci;
-
-                const residueLoci = StructureElement.Loci.extendToWholeResidues(StructureElement.Loci.remap(loci, parent.obj!.data))
-                const residueBundle = StructureElement.Bundle.fromLoci(residueLoci)
-
-                const surroundings = MS.struct.modifier.includeSurroundings({
-                    0: StructureElement.Bundle.toExpression(residueBundle),
-                    radius: this.params.expandRadius,
-                    'as-whole-residues': true
-                });
-
-                const { state, builder, refs } = this.ensureShape(parent);
-
-                builder.to(refs[StructureRepresentationInteractionTags.ResidueSel]!).update(StateTransforms.Model.StructureSelectionFromBundle, old => ({ ...old, bundle: residueBundle }));
-                builder.to(refs[StructureRepresentationInteractionTags.SurrSel]!).update(StateTransforms.Model.StructureSelectionFromExpression, old => ({ ...old, expression: surroundings, label: this.surrLabel }));
-
-                PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
-            }
-        });
-    }
-
-    async update(params: StructureRepresentationInteractionProps) {
-        let oldRadius = this.params.expandRadius;
-        this.params = params;
-
-        const state = this.plugin.state.data;
-        const builder = state.build();
-
-        const all = StateSelection.Generators.root.subtree();
-        for (const repr of state.select(all.withTag(StructureRepresentationInteractionTags.ResidueRepr))) {
-            builder.to(repr).update(this.params.focusParams);
-        }
-        for (const repr of state.select(all.withTag(StructureRepresentationInteractionTags.SurrRepr))) {
-            builder.to(repr).update(this.params.surroundingsParams);
-        }
-        for (const repr of state.select(all.withTag(StructureRepresentationInteractionTags.SurrNciRepr))) {
-            builder.to(repr).update(this.params.nciParams);
-        }
-
-        await PluginCommands.State.Update(this.plugin, { state, tree: builder, options: { doNotLogTiming: true, doNotUpdateCurrent: true } });
-
-        // TODO: update properly
-        if (params.expandRadius !== oldRadius) await this.clear(StateTransform.RootRef);
-
-        return true;
-    }
-}
-
-export const StructureRepresentationInteraction = PluginBehavior.create({
-    name: 'create-structure-representation-interaction',
-    display: { name: 'Structure Representation Interaction' },
-    category: 'interaction',
-    ctor: StructureRepresentationInteractionBehavior,
-    params: (_, plugin) => StructureRepresentationInteractionParams(plugin)
-});

+ 12 - 34
src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -20,17 +20,10 @@ import { CIF } from '../../../../mol-io/reader/cif';
 import { volumeFromDensityServerData } from '../../../../mol-model-formats/volume/density-server';
 import { PluginCommands } from '../../../commands';
 import { StateSelection } from '../../../../mol-state';
-import { Representation } from '../../../../mol-repr/representation';
-import { ButtonsType, ModifiersKeys } from '../../../../mol-util/input/input-observer';
-import { StructureElement, Bond, Structure } from '../../../../mol-model/structure';
+import { StructureElement, Structure } from '../../../../mol-model/structure';
 import { PluginContext } from '../../../context';
-import { Binding } from '../../../../mol-util/binding';
 import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
 
-const B = ButtonsType
-const M = ModifiersKeys
-const Trigger = Binding.Trigger
-
 export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
 
 export namespace VolumeStreaming {
@@ -58,19 +51,14 @@ export namespace VolumeStreaming {
         valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
     };
 
-    export const DefaultBindings = {
-        clickVolumeAroundOnly: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Show Slice', 'Use ${triggers} to show volume around element.'),
-    }
-
-    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, binding?: typeof DefaultBindings, channelParams?: DefaultChannelParams } = { }) {
-        const { data, defaultView, binding, channelParams } = options;
+    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = { }) {
+        const { data, defaultView, channelParams } = options;
         const map = new Map<string, VolumeServerInfo.EntryData>()
         if (data) data.entries.forEach(d => map.set(d.dataId, d))
         const names = data ? data.entries.map(d => [d.dataId, d.dataId] as [string, string]) : []
         const defaultKey = data ? data.entries[0].dataId : ''
         return {
             entry: PD.Mapped<EntryParams>(defaultKey, names, name => PD.Group(createEntryParams({ entryData: map.get(name)!, defaultView, structure: data && data.structure, channelParams }))),
-            bindings: PD.Value(binding || DefaultBindings, { isHidden: true }),
         }
     }
 
@@ -246,28 +234,19 @@ export namespace VolumeStreaming {
                 }
             });
 
-            this.subscribeObservable(this.plugin.behaviors.interaction.click, ({ current, button, modifiers }) => {
-                if (!Binding.match((this.params.bindings && this.params.bindings.clickVolumeAroundOnly) || DefaultBindings.clickVolumeAroundOnly, button, modifiers)) return;
+            this.subscribeObservable(this.plugin.managers.structure.focus.events.changed, () => {
+                const entry = this.plugin.managers.structure.focus.current
+                console.log(entry)
+                const loci = entry ? entry.loci : EmptyLoci
+
                 if (this.params.entry.params.view.name !== 'selection-box') {
-                    this.lastLoci = this.getNormalizedLoci(current.loci);
+                    this.lastLoci = loci;
                 } else {
-                    this.updateInteraction(current);
+                    this.updateInteraction(loci)
                 }
             });
         }
 
-        private getNormalizedLoci(loci: Loci): StructureElement.Loci | EmptyLoci {
-            if (StructureElement.Loci.is(loci)) {
-                return loci;
-            } else if (Bond.isLoci(loci)) {
-                return Bond.toStructureElementLoci(loci);
-            } else if (Structure.isLoci(loci)) {
-                return Structure.toStructureElementLoci(loci.structure);
-            } else {
-                return EmptyLoci;
-            }
-        }
-
         private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
             if (Loci.isEmpty(loci)) {
                 return Box3D.empty();
@@ -286,8 +265,7 @@ export namespace VolumeStreaming {
             return box;
         }
 
-        private updateInteraction(current: Representation.Loci) {
-            const loci = this.getNormalizedLoci(current.loci)
+        private updateInteraction(loci: StructureElement.Loci | EmptyLoci) {
             if (Loci.areEqual(this.lastLoci, loci)) {
                 this.lastLoci = EmptyLoci;
                 this.updateDynamicBox(Box3D.empty());

+ 1 - 2
src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts

@@ -47,7 +47,6 @@ export const InitVolumeStreaming = StateAction.build({
                 serverUrl: PD.Text(plugin.config.get(PluginConfig.VolumeStreaming.DefaultServer) || 'https://ds.litemol.org'),
                 behaviorRef: PD.Text('', { isHidden: true }),
                 emContourProvider: PD.Select<'wwpdb' | 'pdbe'>('wwpdb', [['wwpdb', 'wwPDB'], ['pdbe', 'PDBe']], { isHidden: true }),
-                bindings: PD.Value(VolumeStreaming.DefaultBindings, { isHidden: true }),
                 channelParams: PD.Value<VolumeStreaming.DefaultChannelParams>({}, { isHidden: true })
             })
         };
@@ -103,7 +102,7 @@ export const InitVolumeStreaming = StateAction.build({
     const infoObj = await state.updateTree(infoTree).runInContext(taskCtx);
 
     const behTree = state.build().to(infoTree.ref).apply(CreateVolumeStreamingBehavior,
-        PD.getDefaultValues(VolumeStreaming.createParams({ data: infoObj.data, defaultView: params.defaultView, binding: params.options.bindings, channelParams: params.options.channelParams })),
+        PD.getDefaultValues(VolumeStreaming.createParams({ data: infoObj.data, defaultView: params.defaultView, channelParams: params.options.channelParams })),
         { ref: params.options.behaviorRef ? params.options.behaviorRef : void 0 });
 
     if (params.method === 'em') {

+ 3 - 1
src/mol-plugin/context.ts

@@ -52,6 +52,7 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
 import { Representation } from '../mol-repr/representation';
 import { HierarchyRef } from '../mol-plugin-state/manager/structure/hierarchy-state';
 import { PluginUIComponent } from '../mol-plugin-ui/base';
+import { StructureFocusManager } from '../mol-plugin-state/manager/structure/focus';
 
 export class PluginContext {
     private disposed = false;
@@ -134,7 +135,8 @@ export class PluginContext {
             hierarchy: new StructureHierarchyManager(this),
             component: new StructureComponentManager(this),
             measurement: new StructureMeasurementManager(this),
-            selection: new StructureSelectionManager(this)
+            selection: new StructureSelectionManager(this),
+            focus: new StructureFocusManager(this),
         },
         interactivity: void 0 as any as InteractivityManager,
         camera: new CameraManager(this),

+ 3 - 2
src/mol-plugin/index.ts

@@ -13,7 +13,7 @@ import { StateTransforms } from '../mol-plugin-state/transforms';
 import { VolumeStreamingCustomControls } from '../mol-plugin-ui/custom/volume';
 import { Plugin } from '../mol-plugin-ui/plugin';
 import { PluginBehaviors } from './behavior';
-import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction';
+import { StructureFocusRepresentation } from './behavior/dynamic/selection/structure-focus-representation';
 import { BoxifyVolumeStreaming, CreateVolumeStreamingBehavior, InitVolumeStreaming } from './behavior/dynamic/volume-streaming/transformers';
 import { PluginConfig } from './config';
 import { PluginContext } from './context';
@@ -65,8 +65,9 @@ export const DefaultPluginSpec: PluginSpec = {
         PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
         PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider),
+        PluginSpec.Behavior(PluginBehaviors.Representation.FocusLoci),
         PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci),
-        PluginSpec.Behavior(StructureRepresentationInteraction),
+        PluginSpec.Behavior(StructureFocusRepresentation),
 
         PluginSpec.Behavior(PluginBehaviors.CustomProps.AccessibleSurfaceArea),
         PluginSpec.Behavior(PluginBehaviors.CustomProps.Interactions),