Browse Source

VolumeSourceControls
- initial version, needs more work

David Sehnal 5 years ago
parent
commit
052648023e

+ 1 - 0
src/mol-model/volume/data.ts

@@ -11,6 +11,7 @@ import { equalEps } from '../../mol-math/linear-algebra/3d/common';
 
 /** The basic unit cell that contains the data. */
 interface VolumeData {
+    readonly label?: string,
     readonly cell: SpacegroupCell,
     readonly fractionalBox: Box3D,
     readonly data: Tensor,

+ 14 - 0
src/mol-plugin-state/manager/camera.ts

@@ -11,6 +11,7 @@ import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-ax
 import { Camera } from '../../mol-canvas3d/camera';
 import { Loci } from '../../mol-model/loci';
 import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
+import { GraphicsRenderObject } from '../../mol-gl/render-object';
 
 // TODO: make this customizable somewhere?
 const DefaultCameraFocusOptions = {
@@ -24,6 +25,19 @@ export type CameraFocusOptions = typeof DefaultCameraFocusOptions
 export class CameraManager {
     private boundaryHelper = new BoundaryHelper('98');
 
+    focusRenderObjects(objects?: ReadonlyArray<GraphicsRenderObject>, options?: Partial<CameraFocusOptions>) {
+        if (!objects) return;
+        const spheres: Sphere3D[] = [];
+
+        for (const o of objects) {
+            const s = o.values.boundingSphere.ref.value;
+            if (s.radius === 0) continue;
+            spheres.push(s);
+        }
+
+        this.focusSpheres(spheres, s => s, options);
+    }
+
     focusLoci(loci: Loci | Loci[], options?: Partial<CameraFocusOptions>) {
         // TODO: allow computation of principal axes here?
         // perhaps have an optimized function, that does exact axes small Loci and approximate/sampled from big ones?

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

@@ -25,7 +25,7 @@ import { clearStructureOverpaint, setStructureOverpaint } from '../../helpers/st
 import { createStructureColorThemeParams, createStructureSizeThemeParams } from '../../helpers/structure-representation-params';
 import { StructureSelectionQueries, StructureSelectionQuery } from '../../helpers/structure-selection-query';
 import { StructureRepresentation3D } from '../../transforms/representation';
-import { HierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
+import { StructureHierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
 
 export { StructureComponentManager };
 
@@ -132,7 +132,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         let changed = false;
         const update = this.dataState.build();
 
-        const sync = (r: HierarchyRef) => {
+        const sync = (r: StructureHierarchyRef) => {
             if (!keptRefs.has(r.cell.transform.ref)) {
                 changed = true;
                 update.delete(r.cell);
@@ -169,7 +169,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }
     }
 
-    canBeModified(ref: HierarchyRef) {
+    canBeModified(ref: StructureHierarchyRef) {
         return this.plugin.builders.structure.isComponentTransform(ref.cell);
     }
 
@@ -211,7 +211,7 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
     removeRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot?: StructureRepresentationRef) {
         if (components.length === 0) return;
 
-        const toRemove: HierarchyRef[] = [];
+        const toRemove: StructureHierarchyRef[] = [];
         if (pivot) {
             const index = components[0].representations.indexOf(pivot);
             if (index < 0) return;

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

@@ -21,7 +21,7 @@ export interface StructureHierarchy {
     trajectories: TrajectoryRef[],
     models: ModelRef[],
     structures: StructureRef[],
-    refs: Map<StateTransform.Ref, HierarchyRef>
+    refs: Map<StateTransform.Ref, StructureHierarchyRef>
     // TODO: might be needed in the future
     // decorators: Map<StateTransform.Ref, StateTransform>,
 }
@@ -36,7 +36,7 @@ interface RefBase<K extends string = string, O extends StateObject = StateObject
     version: StateTransform['version']
 }
 
-export type HierarchyRef =
+export type StructureHierarchyRef =
     | TrajectoryRef
     | ModelRef | ModelPropertiesRef | ModelUnitcellRef
     | StructureRef | StructurePropertiesRef | StructureTransformRef | StructureVolumeStreamingRef | StructureComponentRef | StructureRepresentationRef
@@ -140,10 +140,10 @@ function StructureRepresentationRef(cell: StateObjectCell<SO.Molecule.Structure.
 }
 
 export interface GenericRepresentationRef extends RefBase<'generic-representation', SO.Any> {
-    parent: HierarchyRef
+    parent: StructureHierarchyRef
 }
 
-function GenericRepresentationRef(cell: StateObjectCell<SO.Molecule.Structure.Representation3D>, parent: HierarchyRef): GenericRepresentationRef {
+function GenericRepresentationRef(cell: StateObjectCell<SO.Molecule.Structure.Representation3D>, parent: StructureHierarchyRef): GenericRepresentationRef {
     return { kind: 'generic-representation', cell, version: cell.transform.version, parent };
 }
 
@@ -166,7 +166,7 @@ function BuildState(state: State, oldHierarchy: StructureHierarchy): BuildState
     return { state, oldHierarchy, hierarchy: StructureHierarchy(), changed: false, added: new Set() };
 }
 
-function createOrUpdateRefList<R extends HierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, list: R[], ctor: (...args: C) => R, ...args: C) {
+function createOrUpdateRefList<R extends StructureHierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, list: R[], ctor: (...args: C) => R, ...args: C) {
     const ref: R = ctor(...args);
     list.push(ref);
     state.hierarchy.refs.set(cell.transform.ref, ref);
@@ -180,7 +180,7 @@ function createOrUpdateRefList<R extends HierarchyRef, C extends any[]>(state: B
     return ref;
 }
 
-function createOrUpdateRef<R extends HierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, ctor: (...args: C) => R, ...args: C) {
+function createOrUpdateRef<R extends StructureHierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, ctor: (...args: C) => R, ...args: C) {
     const ref: R = ctor(...args);
     state.hierarchy.refs.set(cell.transform.ref, ref);
     const old = state.oldHierarchy.refs.get(cell.transform.ref);
@@ -300,7 +300,7 @@ function isValidCell(cell?: StateObjectCell): cell is StateObjectCell {
     return true;
 }
 
-function isRemoved(this: BuildState, ref: HierarchyRef) {
+function isRemoved(this: BuildState, ref: StructureHierarchyRef) {
     const { cell } = ref;
     if (isValidCell(cell)) return;
     this.changed = true;

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

@@ -12,7 +12,7 @@ import { StateTransform, StateTree } from '../../../mol-state';
 import { SetUtils } from '../../../mol-util/set';
 import { TrajectoryHierarchyPresetProvider } from '../../builder/structure/hierarchy-preset';
 import { PluginComponent } from '../../component';
-import { buildStructureHierarchy, HierarchyRef, ModelRef, StructureComponentRef, StructureHierarchy, StructureRef, TrajectoryRef } from './hierarchy-state';
+import { buildStructureHierarchy, StructureHierarchyRef, ModelRef, StructureComponentRef, StructureHierarchy, StructureRef, TrajectoryRef } from './hierarchy-state';
 
 export class StructureHierarchyManager extends PluginComponent {
     private state = {
@@ -79,7 +79,7 @@ export class StructureHierarchyManager extends PluginComponent {
         return ret;
     }
 
-    private syncCurrent<T extends HierarchyRef>(all: ReadonlyArray<T>, added: Set<StateTransform.Ref>): T[] {
+    private syncCurrent<T extends StructureHierarchyRef>(all: ReadonlyArray<T>, added: Set<StateTransform.Ref>): T[] {
         const current = this.seletionSet;
         const newCurrent: T[] = [];
 
@@ -132,7 +132,7 @@ export class StructureHierarchyManager extends PluginComponent {
         }
     }
 
-    updateCurrent(refs: HierarchyRef[], action: 'add' | 'remove') {
+    updateCurrent(refs: StructureHierarchyRef[], action: 'add' | 'remove') {
         const hierarchy = this.current;
         const set = action === 'add'
             ? SetUtils.union(this.seletionSet, new Set(refs.map(r => r.cell.transform.ref)))
@@ -162,14 +162,14 @@ export class StructureHierarchyManager extends PluginComponent {
         this.behaviors.selection.next({ hierarchy, trajectories, models, structures });
     }
 
-    remove(refs: (HierarchyRef | string)[], canUndo?: boolean) {
+    remove(refs: (StructureHierarchyRef | string)[], canUndo?: boolean) {
         if (refs.length === 0) return;
         const deletes = this.plugin.state.data.build();
         for (const r of refs) deletes.delete(typeof r === 'string' ? r : r.cell.transform.ref);
         return deletes.commit({ canUndo: canUndo ? 'Remove' : false });
     }
 
-    toggleVisibility(refs: ReadonlyArray<HierarchyRef>, action?: 'show' | 'hide') {
+    toggleVisibility(refs: ReadonlyArray<StructureHierarchyRef>, action?: 'show' | 'hide') {
         if (refs.length === 0) return;
 
         const isHidden = action !== void 0

+ 170 - 0
src/mol-plugin-state/manager/volume/hierarchy-state.ts

@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PluginStateObject as SO } from '../../objects';
+import { StateObject, StateTransform, State, StateObjectCell, StateTree, StateTransformer } from '../../../mol-state';
+import { StateTransforms } from '../../transforms';
+
+export function buildVolumeHierarchy(state: State, previous?: VolumeHierarchy) {
+    const build = BuildState(state, previous || VolumeHierarchy());
+    doPreOrder(state.tree, build);
+    if (previous) previous.refs.forEach(isRemoved, build);
+    return { hierarchy: build.hierarchy, added: build.added, changed: build.changed };
+}
+
+export interface VolumeHierarchy {
+    volumes: VolumeRef[],
+    refs: Map<StateTransform.Ref, VolumeHierarchyRef>
+    // TODO: might be needed in the future
+    // decorators: Map<StateTransform.Ref, StateTransform>,
+}
+
+export function VolumeHierarchy(): VolumeHierarchy {
+    return { volumes: [], refs: new Map() };
+}
+
+interface RefBase<K extends string = string, O extends StateObject = StateObject, T extends StateTransformer = StateTransformer> {
+    kind: K,
+    cell: StateObjectCell<O, StateTransform<T>>,
+    version: StateTransform['version']
+}
+
+export type VolumeHierarchyRef = VolumeRef | VolumeRepresentationRef
+
+export interface VolumeRef extends RefBase<'volume', SO.Volume.Data> {
+    representations: VolumeRepresentationRef[]
+}
+
+function VolumeRef(cell: StateObjectCell<SO.Volume.Data>): VolumeRef {
+    return { kind: 'volume', cell, version: cell.transform.version, representations: [] };
+}
+
+export interface VolumeRepresentationRef extends RefBase<'volume-representation', SO.Volume.Representation3D, StateTransforms['Representation']['VolumeRepresentation3D']> {
+    volume: VolumeRef
+}
+
+function VolumeRepresentationRef(cell: StateObjectCell<SO.Volume.Representation3D>, volume: VolumeRef): VolumeRepresentationRef {
+    return { kind: 'volume-representation', cell, version: cell.transform.version, volume };
+}
+
+interface BuildState {
+    state: State,
+    oldHierarchy: VolumeHierarchy,
+
+    hierarchy: VolumeHierarchy,
+
+    currentVolume?: VolumeRef,
+
+    changed: boolean,
+    added: Set<StateTransform.Ref>
+}
+
+function BuildState(state: State, oldHierarchy: VolumeHierarchy): BuildState {
+    return { state, oldHierarchy, hierarchy: VolumeHierarchy(), changed: false, added: new Set() };
+}
+
+function createOrUpdateRefList<R extends VolumeHierarchyRef, C extends any[]>(state: BuildState, cell: StateObjectCell, list: R[], ctor: (...args: C) => R, ...args: C) {
+    const ref: R = ctor(...args);
+    list.push(ref);
+    state.hierarchy.refs.set(cell.transform.ref, ref);
+    const old = state.oldHierarchy.refs.get(cell.transform.ref);
+    if (old) {
+        if (old.version !== cell.transform.version) state.changed = true;
+    } else {
+        state.added.add(ref.cell.transform.ref);
+        state.changed = true;
+    }
+    return ref;
+}
+
+type TestCell = (cell: StateObjectCell, state: BuildState) => boolean
+type ApplyRef = (state: BuildState, cell: StateObjectCell) => boolean | void
+type LeaveRef = (state: BuildState) => any
+
+function isType(t: StateObject.Ctor): TestCell {
+    return (cell) => t.is(cell.obj);
+}
+
+function noop() { }
+
+const Mapping: [TestCell, ApplyRef, LeaveRef][] = [
+    [isType(SO.Volume.Data), (state, cell) => {
+        state.currentVolume = createOrUpdateRefList(state, cell, state.hierarchy.volumes, VolumeRef, cell);
+    }, state => state.currentVolume = void 0],
+
+    [(cell, state) => {
+        return !cell.state.isGhost && !!state.currentVolume && SO.Volume.Representation3D.is(cell.obj);
+    }, (state, cell) => {
+        if (state.currentVolume) {
+            createOrUpdateRefList(state, cell, state.currentVolume.representations, VolumeRepresentationRef, cell, state.currentVolume);
+        }
+        return false;
+    }, noop]
+];
+
+function isValidCell(cell?: StateObjectCell): cell is StateObjectCell {
+    if (!cell || !cell?.parent || !cell.parent.cells.has(cell.transform.ref)) return false;
+    const { obj } = cell;
+    if (!obj || obj === StateObject.Null || (cell.status !== 'ok' && cell.status !== 'error')) return false;
+    return true;
+}
+
+function isRemoved(this: BuildState, ref: VolumeHierarchyRef) {
+    const { cell } = ref;
+    if (isValidCell(cell)) return;
+    this.changed = true;
+}
+
+type VisitorCtx = { tree: StateTree, state: BuildState };
+
+function _preOrderFunc(this: VisitorCtx, c: StateTransform.Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); }
+function _doPreOrder(ctx: VisitorCtx, root: StateTransform) {
+    const { state } = ctx;
+    const cell = state.state.cells.get(root.ref);
+    if (!isValidCell(cell)) return;
+
+    let onLeave: undefined | ((state: BuildState) => any) = void 0;
+    let end = false;
+    for (const [test, f, l] of Mapping) {
+        if (test(cell, state)) {
+            const cont = f(state, cell);
+            if (cont === false) {
+                end = true;
+                break;
+            }
+            onLeave = l;
+            break;
+        }
+    }
+
+    // TODO: might be needed in the future
+    // const { currentComponent, currentModel, currentStructure, currentTrajectory } = ctx.state;
+    // const inTrackedSubtree = currentComponent || currentModel || currentStructure || currentTrajectory;
+
+    // if (inTrackedSubtree && cell.transform.transformer.definition.isDecorator) {
+    //     const ref = cell.transform.ref;
+    //     const old = ctx.state.oldHierarchy.decorators.get(ref);
+    //     if (old && old.version !== cell.transform.version) {
+    //         ctx.state.changed = true;
+    //     }
+    //     ctx.state.hierarchy.decorators.set(cell.transform.ref, cell.transform);
+    // }
+
+    if (end) return;
+
+    const children = ctx.tree.children.get(root.ref);
+    if (children && children.size) {
+        children.forEach(_preOrderFunc, ctx);
+    }
+
+    if (onLeave) onLeave(state);
+}
+
+function doPreOrder(tree: StateTree, state: BuildState): BuildState {
+    const ctx: VisitorCtx = { tree, state };
+    _doPreOrder(ctx, tree.root);
+    return ctx.state;
+}

+ 134 - 0
src/mol-plugin-state/manager/volume/hierarchy.ts

@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
+import { PluginContext } from '../../../mol-plugin/context';
+import { PluginComponent } from '../../component';
+import { buildVolumeHierarchy, VolumeHierarchy, VolumeHierarchyRef, VolumeRef } from './hierarchy-state';
+import { createVolumeRepresentationParams } from '../../helpers/volume-representation-params';
+import { StateTransforms } from '../../transforms';
+
+export class VolumeHierarchyManager extends PluginComponent {
+    private state = {
+        syncedTree: this.dataState.tree,
+        notified: false,
+
+        hierarchy: VolumeHierarchy(),
+        selection: void 0 as VolumeRef | undefined
+    }
+
+    readonly behaviors = {
+        selection: this.ev.behavior({
+            hierarchy: this.current,
+            volume: this.selection
+        })
+    }
+
+    private get dataState() {
+        return this.plugin.state.data;
+    }
+
+    get current() {
+        this.sync(false);
+        return this.state.hierarchy;
+    }
+
+    get selection() {
+        this.sync(false);
+        return this.state.selection;
+    }
+
+    private sync(notify: boolean) {
+        if (!notify && this.dataState.inUpdate) return;
+
+        if (this.state.syncedTree === this.dataState.tree) {
+            if (notify && !this.state.notified) {
+                this.state.notified = true;
+                this.behaviors.selection.next({ hierarchy: this.state.hierarchy, volume: this.state.selection });
+            }
+
+            return;
+        }
+
+        this.state.syncedTree = this.dataState.tree;
+
+        const update = buildVolumeHierarchy(this.plugin.state.data, this.current);
+        if (!update.changed) {
+            return;
+        }
+
+        const { hierarchy } = update;
+
+        this.state.hierarchy = hierarchy;
+        if (!this.state.selection) {
+            this.state.selection = hierarchy.volumes[0];
+        } else {
+            this.state.selection = hierarchy.refs.has(this.state.selection.cell.transform.ref) ? hierarchy.refs.get(this.state.selection.cell.transform.ref) as VolumeRef : hierarchy.volumes[0];
+        }
+
+        if (notify) {
+            this.state.notified = true;
+            this.behaviors.selection.next({ hierarchy, volume: this.state.selection });
+        } else {
+            this.state.notified = false;
+        }
+    }
+
+    setCurrent(volume?: VolumeRef) {
+        this.behaviors.selection.next({ hierarchy: this.state.hierarchy, volume: volume || this.state.hierarchy.volumes[0] });
+    }
+
+    // TODO: have common util
+    remove(refs: (VolumeHierarchyRef | string)[], canUndo?: boolean) {
+        if (refs.length === 0) return;
+        const deletes = this.plugin.state.data.build();
+        for (const r of refs) deletes.delete(typeof r === 'string' ? r : r.cell.transform.ref);
+        return deletes.commit({ canUndo: canUndo ? 'Remove' : false });
+    }
+
+    // TODO: have common util
+    toggleVisibility(refs: ReadonlyArray<VolumeHierarchyRef>, action?: 'show' | 'hide') {
+        if (refs.length === 0) return;
+
+        const isHidden = action !== void 0
+            ? (action === 'show' ? false : true)
+            : !refs[0].cell.state.isHidden;
+        for (const c of refs) {
+            setSubtreeVisibility(this.dataState, c.cell.transform.ref, isHidden);
+        }
+    }
+
+    addRepresentation(ref: VolumeRef, type: string) {
+        const update = this.dataState.build()
+            .to(ref.cell)
+            .apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, ref.cell.obj?.data, {
+                type: type as any,
+            }));
+
+        return update.commit({ canUndo: 'Add Representation' });
+    }
+
+    constructor(private plugin: PluginContext) {
+        super();
+
+        this.subscribe(plugin.state.data.events.changed, e => {
+            if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return;
+            this.sync(true);
+        });
+
+        this.subscribe(plugin.behaviors.state.isAnimating, isAnimating => {
+            if (!isAnimating && !plugin.behaviors.state.isUpdating.value) this.sync(true);
+        });
+    }
+}
+
+export namespace VolumeHierarchyManager {
+    export function getRepresentationTypes(plugin: PluginContext, pivot: VolumeRef | undefined) {
+        return pivot?.cell.obj?.data
+            ? plugin.representation.volume.registry.getApplicableTypes(pivot.cell.obj?.data!)
+            : plugin.representation.volume.registry.types;
+    }
+}

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

@@ -28,7 +28,7 @@ import { StructureComponentControls } from './structure/components';
 import { StructureMeasurementsControls } from './structure/measurements';
 import { StructureSelectionActionsControls } from './structure/selection';
 import { StructureSourceControls } from './structure/source';
-import { VolumeStreamingControls } from './structure/volume';
+import { VolumeStreamingControls, VolumeSourceControls } from './structure/volume';
 
 export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: boolean, label: string }> {
     state = { show: false, label: '' }
@@ -298,6 +298,7 @@ export class DefaultStructureTools extends PluginUIComponent {
             <StructureMeasurementsControls />
             <StructureComponentControls />
             <VolumeStreamingControls />
+            <VolumeSourceControls />
 
             <CustomStructureControls />
         </>;

+ 0 - 2
src/mol-plugin-ui/structure/components.tsx

@@ -215,8 +215,6 @@ class ComponentListControls extends PurePluginUIComponent {
     }
 }
 
-
-
 type StructureComponentEntryActions = 'action' | 'remove'
 
 class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureComponentRef[] }, { action?: StructureComponentEntryActions }> {

+ 2 - 2
src/mol-plugin-ui/structure/generic.tsx

@@ -9,7 +9,7 @@ import MoreHoriz from '@material-ui/icons/MoreHoriz';
 import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined';
 import VisibilityOffOutlined from '@material-ui/icons/VisibilityOffOutlined';
 import * as React from 'react';
-import { HierarchyRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
+import { StructureHierarchyRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { PluginCommands } from '../../mol-plugin/commands';
 import { State } from '../../mol-state';
 import { PurePluginUIComponent } from '../base';
@@ -62,7 +62,7 @@ export class GenericEntryListControls extends PurePluginUIComponent {
     }
 }
 
-export class GenericEntry<T extends HierarchyRef> extends PurePluginUIComponent<{ refs: T[], labelMultiple?: string }, { showOptions: boolean }> {
+export class GenericEntry<T extends StructureHierarchyRef> extends PurePluginUIComponent<{ refs: T[], labelMultiple?: string }, { showOptions: boolean }> {
     state = { showOptions: false }
 
     componentDidMount() {

+ 5 - 5
src/mol-plugin-ui/structure/source.tsx

@@ -6,7 +6,7 @@
  */
 
 import * as React from 'react';
-import { HierarchyRef, ModelRef, TrajectoryRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
+import { StructureHierarchyRef, ModelRef, TrajectoryRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
 import { CollapsableControls, CollapsableState } from '../base';
 import { ActionMenu } from '../controls/action-menu';
@@ -41,7 +41,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
         });
     }
 
-    private item = (ref: HierarchyRef) => {
+    private item = (ref: StructureHierarchyRef) => {
         const selected = this.plugin.managers.structure.hierarchy.seletionSet;
 
         let label;
@@ -179,11 +179,11 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
     }
 
     selectHierarchy: ActionMenu.OnSelectMany = (items) => {
-        if (!items || items.length === 0) return 0;
+        if (!items || items.length === 0) return;
 
-        const refs: HierarchyRef[] = [];
+        const refs: StructureHierarchyRef[] = [];
         for (const i of items) {
-            for (const r of (i.value as HierarchyRef[])) refs.push(r);
+            for (const r of (i.value as StructureHierarchyRef[])) refs.push(r);
         }
 
         this.plugin.managers.structure.hierarchy.updateCurrent(refs, items[0].selected ? 'remove' : 'add');

+ 178 - 9
src/mol-plugin-ui/structure/volume.tsx

@@ -5,20 +5,29 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import Add from '@material-ui/icons/Add';
+import BlurOn from '@material-ui/icons/BlurOn';
+import Check from '@material-ui/icons/Check';
+import ErrorSvg from '@material-ui/icons/Error';
+import DeleteOutlined from '@material-ui/icons/DeleteOutlined';
+import MoreHoriz from '@material-ui/icons/MoreHoriz';
+import VisibilityOffOutlined from '@material-ui/icons/VisibilityOffOutlined';
+import VisibilityOutlined from '@material-ui/icons/VisibilityOutlined';
 import * as React from 'react';
+import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
+import { VolumeHierarchyManager } from '../../mol-plugin-state/manager/volume/hierarchy';
+import { VolumeRef, VolumeRepresentationRef } from '../../mol-plugin-state/manager/volume/hierarchy-state';
+import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
+import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
 import { InitVolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
-import { CollapsableControls, CollapsableState } from '../base';
+import { State, StateSelection, StateTransform } from '../../mol-state';
+import { CollapsableControls, CollapsableState, PurePluginUIComponent } from '../base';
+import { ActionMenu } from '../controls/action-menu';
+import { Button, ExpandGroup, IconButton } from '../controls/common';
 import { ApplyActionControl } from '../state/apply-action';
 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';
-import { StateSelection, StateTransform } from '../../mol-state';
-import { VolumeStreaming } from '../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
-import Check from '@material-ui/icons/Check';
-import ErrorSvg from '@material-ui/icons/Error';
-import BlurOn from '@material-ui/icons/BlurOn';
+import { PluginCommands } from '../../mol-plugin/commands';
 
 interface VolumeStreamingControlState extends CollapsableState {
     isBusy: boolean
@@ -98,4 +107,164 @@ export class VolumeStreamingControls extends CollapsableControls<{}, VolumeStrea
         if (!pivot.volumeStreaming) return this.renderEnable();
         return this.renderParams();
     }
+}
+
+interface VolumeSourceControlState extends CollapsableState {
+    isBusy: boolean,
+    show?: 'hierarchy' | 'add-repr'
+}
+
+export class VolumeSourceControls extends CollapsableControls<{}, VolumeSourceControlState> {
+    protected defaultState(): VolumeSourceControlState {
+        return {
+            header: 'Volume',
+            isCollapsed: false,
+            isBusy: false,
+            isHidden: true,
+            brand: { accent: 'purple', svg: BlurOn }
+        };
+    }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.managers.volume.hierarchy.behaviors.selection, sel => {
+            this.setState({ isHidden: sel.hierarchy.volumes.length === 0 });
+        });
+        this.subscribe(this.plugin.behaviors.state.isBusy, v => {
+            this.setState({ isBusy: v });
+        });
+    }
+
+    private item = (ref: VolumeRef) => {
+        const selected = this.plugin.managers.volume.hierarchy.selection;
+
+        const label = ref.cell.obj?.data.label || 'Volume';
+        const item: ActionMenu.Item = { kind: 'item', label: label || ref.kind, selected: selected === ref, value: ref };
+        return item;
+    }
+
+    get hierarchyItems() {
+        const mng = this.plugin.managers.volume.hierarchy;
+        const { current } = mng;
+        const ret: ActionMenu.Items = [];
+        for (let ref of current.volumes) {
+            ret.push(this.item(ref));
+        }
+        return ret;
+    }
+
+    get addActions(): ActionMenu.Items {
+        const mng = this.plugin.managers.volume.hierarchy;
+        const current = mng.selection;
+
+        const ret: ActionMenu.Items = [
+            ...VolumeHierarchyManager.getRepresentationTypes(this.plugin, current)
+                .map(t => ActionMenu.Item(t[1], () => mng.addRepresentation(current!, t[0])))
+        ];
+
+        return ret;
+    }
+
+    get isEmpty() {
+        const { volumes } = this.plugin.managers.volume.hierarchy.current;
+        return volumes.length === 0;
+    }
+
+    get label() {
+        const selected = this.plugin.managers.volume.hierarchy.selection;
+        if (!selected) return 'Nothing Selected';
+        return selected?.cell.obj?.data.label || 'Volume';
+    }
+
+    selectCurrent: ActionMenu.OnSelect = (item) => {
+        this.toggleHierarchy();
+        if (!item) return;
+        this.plugin.managers.volume.hierarchy.setCurrent(item.value as VolumeRef);
+    }
+
+    selectAdd: ActionMenu.OnSelect = (item) => {
+        if (!item) return;
+        this.setState({ show: void 0 });
+        (item.value as any)();
+    }
+
+    toggleHierarchy = () => this.setState({ show: this.state.show !== 'hierarchy' ? 'hierarchy' : void 0 });
+    toggleAddRepr = () => this.setState({ show: this.state.show !== 'add-repr' ? 'add-repr' : void 0 });
+
+    renderControls() {
+        const disabled = this.state.isBusy || this.isEmpty;
+        const label = this.label;
+
+        const selected = this.plugin.managers.volume.hierarchy.selection;
+
+        return <>
+            <div className='msp-flex-row' style={{ marginTop: '1px' }}>
+                <Button noOverflow flex onClick={this.toggleHierarchy} disabled={disabled} title={label}>{label}</Button>
+                {!this.isEmpty && <IconButton svg={Add} onClick={this.toggleAddRepr} title='Apply a structure presets to the current hierarchy.' toggleState={this.state.show === 'add-repr'} disabled={disabled} />}
+            </div>
+            {this.state.show === 'hierarchy' && <ActionMenu items={this.hierarchyItems} onSelect={this.selectCurrent} />}
+            {this.state.show === 'add-repr' && <ActionMenu items={this.addActions} onSelect={this.selectAdd} />}
+
+            {selected && selected.representations.length > 0 && <div style={{ marginTop: '6px' }}>
+                {selected.representations.map(r => <VolumeRepresentationControls key={r.cell.transform.ref} representation={r} />)}
+            </div>}
+        </>;
+    }
+}
+
+type VolumeRepresentationEntryActions = 'update'
+
+class VolumeRepresentationControls extends PurePluginUIComponent<{ representation: VolumeRepresentationRef }, { action?: VolumeRepresentationEntryActions }> {
+    state = { action: void 0 as VolumeRepresentationEntryActions | undefined }
+
+    componentDidMount() {
+        this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
+            if (State.ObjectEvent.isCell(e, this.props.representation.cell)) this.forceUpdate();
+        });
+    }
+
+    remove = () => this.plugin.managers.volume.hierarchy.remove([ this.props.representation ], true);
+
+    toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        e.currentTarget.blur();
+        this.plugin.managers.volume.hierarchy.toggleVisibility([ this.props.representation ]);
+    }
+
+    toggleUpdate = () => this.setState({ action: this.state.action === 'update' ? void 0 : 'update' });
+
+    highlight = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        if (!this.props.representation.cell.parent) return;
+        PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.representation.cell.parent!, ref: this.props.representation.cell.transform.ref });
+    }
+
+    clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
+        e.preventDefault();
+        PluginCommands.Interactivity.ClearHighlights(this.plugin);
+    }
+
+    focus = () => {
+        const repr = this.props.representation;
+        const objects = this.props.representation.cell.obj?.data.repr.renderObjects;
+        if (repr.cell.state.isHidden) this.plugin.managers.volume.hierarchy.toggleVisibility([this.props.representation], 'show');
+        this.plugin.managers.camera.focusRenderObjects(objects, { extraRadius: 1 });
+    }
+
+    render() {
+        const repr = this.props.representation.cell;
+        return <>
+            <div className='msp-flex-row'>
+                <Button noOverflow className='msp-control-button-label' title={`${repr.obj?.label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
+                    {repr.obj?.label}
+                    <small className='msp-25-lower-contrast-text' style={{ float: 'right' }}>{repr.obj?.description}</small>
+                </Button>
+                <IconButton svg={repr.state.isHidden ? VisibilityOffOutlined : VisibilityOutlined} toggleState={false} onClick={this.toggleVisible} title={`${repr.state.isHidden ? 'Show' : 'Hide'} component`} small className='msp-form-control' flex />
+                <IconButton svg={DeleteOutlined} onClick={this.remove} title='Remove' small />
+                <IconButton svg={MoreHoriz} onClick={this.toggleUpdate} title='Actions' toggleState={this.state.action === 'update'} />
+            </div>
+            {this.state.action === 'update' && !!repr.parent && <div style={{ marginBottom: '6px' }} className='msp-accent-offset'>
+                <UpdateTransformControl state={repr.parent} transform={repr.transform} customHeader='none' noMargin />
+            </div>}
+        </>;
+    }
 }

+ 6 - 2
src/mol-plugin/context.ts

@@ -21,7 +21,7 @@ import { LociLabel, LociLabelManager } from '../mol-plugin-state/manager/loci-la
 import { StructureComponentManager } from '../mol-plugin-state/manager/structure/component';
 import { StructureFocusManager } from '../mol-plugin-state/manager/structure/focus';
 import { StructureHierarchyManager } from '../mol-plugin-state/manager/structure/hierarchy';
-import { HierarchyRef } from '../mol-plugin-state/manager/structure/hierarchy-state';
+import { StructureHierarchyRef } from '../mol-plugin-state/manager/structure/hierarchy-state';
 import { StructureMeasurementManager } from '../mol-plugin-state/manager/structure/measurement';
 import { StructureSelectionManager } from '../mol-plugin-state/manager/structure/selection';
 import { PluginUIComponent } from '../mol-plugin-ui/base';
@@ -57,6 +57,7 @@ import { AssetManager } from '../mol-util/assets';
 import { PluginStateSnapshotManager } from '../mol-plugin-state/manager/snapshots';
 import { PluginAnimationManager } from '../mol-plugin-state/manager/animation';
 import { objectForEach } from '../mol-util/object';
+import { VolumeHierarchyManager } from '../mol-plugin-state/manager/volume/hierarchy';
 
 export class PluginContext {
     runTask = <T>(task: Task<T>) => this.tasks.run(task);
@@ -137,6 +138,9 @@ export class PluginContext {
             selection: new StructureSelectionManager(this),
             focus: new StructureFocusManager(this),
         },
+        volume: {
+            hierarchy: new VolumeHierarchyManager(this)
+        },
         interactivity: void 0 as any as InteractivityManager,
         camera: new CameraManager(this),
         animation: new PluginAnimationManager(this),
@@ -151,7 +155,7 @@ export class PluginContext {
     readonly customParamEditors = new Map<string, StateTransformParameters.Class>();
 
     readonly customStructureControls = new Map<string, { new(): PluginUIComponent<any, any, any> }>();
-    readonly genericRepresentationControls = new Map<string, (selection: StructureHierarchyManager['selection']) => [HierarchyRef[], string]>();
+    readonly genericRepresentationControls = new Map<string, (selection: StructureHierarchyManager['selection']) => [StructureHierarchyRef[], string]>();
 
     readonly helpers = {
         substructureParent: new SubstructureParentHelper(this),