Bladeren bron

wip refactoring mol-state

David Sehnal 6 jaren geleden
bovenliggende
commit
92412f21ce

+ 1 - 1
src/apps/basic-wrapper/index.ts

@@ -50,7 +50,7 @@ class BasicWrapper {
 
         return parsed
             .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 

+ 1 - 1
src/apps/viewer/extensions/jolecule.ts

@@ -57,7 +57,7 @@ interface JoleculeSnapshot {
 
 function createTemplate(plugin: PluginContext, state: State, id: string) {
     const b = new StateBuilder.Root(state.tree);
-    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }});
+    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { state: { isGhost: true }});
     const model = createModelTree(data, 'cif');
     const structure = model.apply(StateTransforms.Model.StructureFromModel, {});
     complexRepresentation(plugin, structure, { hideWater: true });

+ 1 - 1
src/examples/proteopedia-wrapper/index.ts

@@ -73,7 +73,7 @@ class MolStarProteopediaWrapper {
         const model = this.state.build().to('model');
 
         return model
-            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', state: { isGhost: false } })
             .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
     }
 

+ 1 - 1
src/mol-plugin/behavior/dynamic/labels.ts

@@ -118,7 +118,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({
             for (const s of structures) {
                 const rootStructure = getRootStructure(s, state)
                 if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue
-                if (!state.cellStates.get(s.transform.ref).isHidden) {
+                if (!s.state.isHidden) {
                     rootStructures.add(rootStructure.obj)
                 }
             }

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

@@ -56,30 +56,30 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
         if (!refs['structure-interaction-group']) {
             refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup,
-                { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref;
+                { label: 'Current Interaction' }, { tags: Tags.Group }).ref;
         }
 
         // Selections
         if (!refs[Tags.ResidueSel]) {
             refs[Tags.ResidueSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Residue' }, { props: { tag: Tags.ResidueSel } }).ref;
+                { query: { } as any, label: 'Residue' }, { tags: Tags.ResidueSel }).ref;
         }
 
         if (!refs[Tags.SurrSel]) {
             refs[Tags.SurrSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection,
-                { query: { } as any, label: 'Surroundings' }, { props: { tag: Tags.SurrSel } }).ref;
+                { query: { } as any, label: 'Surroundings' }, { tags: Tags.SurrSel }).ref;
         }
 
         // Representations
         // TODO: ability to customize how it looks in the behavior params
         if (!refs[Tags.ResidueRepr]) {
             refs[Tags.ResidueRepr] = builder.to(refs['structure-interaction-residue-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createResVisualParams(cell.obj!.data), { props: { tag: Tags.ResidueRepr } }).ref;
+                this.createResVisualParams(cell.obj!.data), { tags: Tags.ResidueRepr }).ref;
         }
 
         if (!refs[Tags.SurrRepr]) {
             refs[Tags.SurrRepr] = builder.to(refs['structure-interaction-surr-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D,
-                this.createSurVisualParams(cell.obj!.data), { props: { tag: Tags.SurrRepr } }).ref;
+                this.createSurVisualParams(cell.obj!.data), { tags: Tags.SurrRepr }).ref;
         }
 
         return { state, builder, refs };
@@ -87,7 +87,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
 
     private clear(root: StateTransform.Ref) {
         const state = this.plugin.state.dataState;
-        const groups = state.select(StateSelection.Generators.byRef(root).subtree().filter(o => o.transform.props.tag === Tags.Group));
+        const groups = state.select(StateSelection.Generators.byRef(root).subtree().withTag(Tags.Group));
         if (groups.length === 0) return;
 
         const update = state.build();

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

@@ -59,11 +59,11 @@ export const InitVolumeStreaming = StateAction.build({
         PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data)));
 
     if (params.method === 'em') {
-        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true } });
     } else {
-        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } });
-        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true } });
+        behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true } });
     }
     await state.updateTree(behTree).runInContext(taskCtx);
 }));

+ 6 - 6
src/mol-plugin/behavior/static/representation.ts

@@ -7,7 +7,7 @@
 import { PluginStateObject as SO } from '../../state/objects';
 import { PluginContext } from 'mol-plugin/context';
 import { Representation } from 'mol-repr/representation';
-import { State } from 'mol-state';
+import { StateObjectCell } from 'mol-state';
 
 export function registerDefault(ctx: PluginContext) {
     SyncRepresentationToCanvas(ctx);
@@ -21,7 +21,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
     const events = ctx.state.dataState.events;
     events.object.created.subscribe(e => {
         if (!SO.isRepresentation3D(e.obj)) return;
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         e.obj.data.repr.setState({ syncManually: true });
         ctx.canvas3d.add(e.obj.data.repr);
 
@@ -39,7 +39,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) {
             return;
         }
 
-        updateVisibility(e, e.obj.data.repr);
+        updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr);
         if (e.action === 'recreate') {
             e.obj.data.repr.setState({ syncManually: true });
         }
@@ -86,11 +86,11 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
     ctx.state.dataState.events.cell.stateUpdated.subscribe(e => {
         const cell = e.state.cells.get(e.ref)!;
         if (!SO.isRepresentation3D(cell.obj)) return;
-        updateVisibility(e, cell.obj.data.repr);
+        updateVisibility(cell, cell.obj.data.repr);
         ctx.canvas3d.requestDraw(true);
     })
 }
 
-function updateVisibility(e: State.ObjectEvent, r: Representation<any>) {
-    r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden });
+function updateVisibility(cell: StateObjectCell, r: Representation<any>) {
+    r.setState({ visible: !cell.state.isHidden });
 }

+ 3 - 2
src/mol-plugin/behavior/static/state.ts

@@ -72,7 +72,8 @@ export function RemoveObject(ctx: PluginContext) {
                 const children = tree.children.get(curr.parent);
                 if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref);
                 const parent = tree.transforms.get(curr.parent);
-                if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref);
+                // TODO: should this use "cell state" instead?
+                if (!parent.state.isGhost) return remove(state, curr.ref);
                 curr = parent;
             }
         } else {
@@ -86,7 +87,7 @@ export function ToggleExpanded(ctx: PluginContext) {
 }
 
 export function ToggleVisibility(ctx: PluginContext) {
-    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden));
+    PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cells.get(ref)!.state.isHidden));
 }
 
 function setVisibility(state: State, root: StateTransform.Ref, value: boolean) {

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

@@ -180,7 +180,7 @@ export class PluginContext {
         const tree = this.state.behaviorState.build();
 
         for (const cat of Object.keys(PluginBehavior.Categories)) {
-            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } });
+            tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, state: { isLocked: true } });
         }
 
         for (const b of this.spec.behaviors) {

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

@@ -146,7 +146,7 @@ const DownloadStructure = StateAction.build({
         createStructureTree(ctx, traj, supportProps);
     } else {
         for (const download of downloadParams) {
-            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } });
+            const data = b.toRoot().apply(StateTransforms.Data.Download, download, { state: { isGhost: true } });
             const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif');
             createStructureTree(ctx, traj, supportProps)
         }
@@ -179,14 +179,14 @@ export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary
     let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory>
     switch (format) {
         case 'cif':
-            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } })
-                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } })
+            parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } })
+                .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { state: { isGhost: true } })
             break
         case 'pdb':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { state: { isGhost: true } });
             break
         case 'gro':
-            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } });
+            parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { state: { isGhost: true } });
             break
         default:
             throw new Error('unsupported format')

+ 4 - 4
src/mol-plugin/state/animation/built-in.ts

@@ -121,7 +121,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
 
             changed = true;
             update.to(r)
-                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } });
+                .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { tags: 'animate-assembly-unwind' });
         }
 
         if (!changed) return;
@@ -131,7 +131,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-assembly-unwind'));
+            .withTag('animate-assembly-unwind'));
         if (reprs.length === 0) return;
 
         const update = state.build();
@@ -191,7 +191,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
 
             changed = true;
             update.to(r.transform.ref)
-                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { props: { tag: 'animate-units-explode' } });
+                .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { tags: 'animate-units-explode' });
         }
 
         if (!changed) return;
@@ -201,7 +201,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
     async teardown(_, plugin) {
         const state = plugin.state.dataState;
         const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.props.tag === 'animate-units-explode'));
+            .withTag('animate-units-explode'));
         if (reprs.length === 0) return;
 
         const update = state.build();

+ 4 - 1
src/mol-plugin/ui/base.tsx

@@ -58,4 +58,7 @@ export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends Rea
         this.plugin = context;
         if (this.init) this.init();
     }
-}
+}
+
+export type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
+export type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never

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

@@ -16,6 +16,7 @@ import * as React from 'react';
 import LineGraphComponent from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
 import { NumericInput, IconButton } from './common';
+import { _Props, _State } from '../base';
 
 export interface ParameterControlsProps<P extends PD.Params = PD.Params> {
     params: P,
@@ -513,9 +514,6 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any>
     }
 }
 
-type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never
-type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never
-
 class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> {
     state = { params: {}, value: void 0 as any, current: void 0 as any };
 

+ 56 - 59
src/mol-plugin/ui/state/tree.tsx

@@ -6,9 +6,9 @@
 
 import * as React from 'react';
 import { PluginStateObject } from 'mol-plugin/state/objects';
-import { State, StateObject, StateTransform } from 'mol-state'
+import { State, StateObject, StateTransform, StateObjectCell } from 'mol-state'
 import { PluginCommands } from 'mol-plugin/command';
-import { PluginUIComponent } from '../base';
+import { PluginUIComponent, _Props, _State } from '../base';
 import { StateObjectActions } from './actions';
 
 export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
@@ -37,74 +37,72 @@ export class StateTree extends PluginUIComponent<{ state: State }, { showActions
         if (this.state.showActions) {
             return <StateObjectActions state={this.props.state} nodeRef={ref} hideHeader={true} />
         }
-        return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />;
+        return <StateTreeNode cell={this.props.state.cells.get(ref)!} depth={0} />;
     }
 }
 
-class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> {
+class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, { isCollapsed: boolean }> {
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
     }
 
-    get cellState() {
-        return this.props.state.cellStates.get(this.props.nodeRef);
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
         this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
-            if (this.is(e) && e.state.transforms.has(this.props.nodeRef)) {
-                this.setState({ isCollapsed: e.cellState.isCollapsed });
+            if (this.props.cell === e.cell && this.is(e) && e.state.cells.has(this.ref)) {
+                if (!!this.props.cell.state.isCollapsed !== this.state.isCollapsed) {
+                    this.setState({ isCollapsed: !!e.cell.state.isCollapsed });
+                }
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.created, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.cell.transform.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.cell.transform.parent) {
                 this.forceUpdate();
             }
         });
 
         this.subscribe(this.plugin.events.state.cell.removed, e => {
-            if (this.props.state === e.state && this.props.nodeRef === e.parent) {
+            if (this.props.cell.parent === e.state && this.ref === e.parent) {
                 this.forceUpdate();
             }
         });
     }
 
     state = {
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null {
+        if (!!props.cell.state.isCollapsed === state.isCollapsed) return null;
+        return { isCollapsed: !!props.cell.state.isCollapsed };
     }
 
     render() {
-        const cell = this.props.state.cells.get(this.props.nodeRef);
+        const cell = this.props.cell;
         if (!cell || cell.obj === StateObject.Null) return null;
 
-        const cellState = this.cellState;
-        const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost;
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
+        const cellState = cell.state;
+        const showLabel = cell.status !== 'ok' || !cell.state.isGhost;
+        const children = cell.parent.tree.children.get(this.ref);
         const newDepth = showLabel ? this.props.depth + 1 : this.props.depth;
 
         if (!showLabel) {
             if (children.size === 0) return null;
             return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
             </div>;
         }
 
         return <>
-            <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} />
+            <StateTreeNodeLabel cell={cell} depth={this.props.depth} />
             {children.size === 0
                 ? void 0
                 : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}>
-                    {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)}
+                    {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)}
                 </div>
             }
         </>;
@@ -112,11 +110,15 @@ class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, d
 }
 
 class StateTreeNodeLabel extends PluginUIComponent<
-    { nodeRef: string, state: State, depth: number },
-    { state: State, isCurrent: boolean, isCollapsed: boolean /*, updaterCollapsed: boolean */ }> {
+    { cell: StateObjectCell, depth: number },
+    { isCurrent: boolean, isCollapsed: boolean }> {
 
     is(e: State.ObjectEvent) {
-        return e.ref === this.props.nodeRef && e.state === this.props.state;
+        return e.ref === this.ref && e.state === this.props.cell.parent;
+    }
+
+    get ref() {
+        return this.props.cell.transform.ref;
     }
 
     componentDidMount() {
@@ -126,70 +128,66 @@ class StateTreeNodeLabel extends PluginUIComponent<
 
         this.subscribe(this.plugin.state.behavior.currentObject, e => {
             if (!this.is(e)) {
-                if (this.state.isCurrent && e.state.transforms.has(this.props.nodeRef)) {
-                    this.setState({ isCurrent: this.props.state.current === this.props.nodeRef });
+                if (this.state.isCurrent && e.state.transforms.has(this.ref)) {
+                    this.setState({ isCurrent: this.props.cell.parent.current === this.ref });
                 }
                 return;
             }
 
-            if (e.state.transforms.has(this.props.nodeRef)) {
+            if (e.state.transforms.has(this.ref)) {
                 this.setState({
-                    isCurrent: this.props.state.current === this.props.nodeRef,
-                    isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed
+                    isCurrent: this.props.cell.parent.current === this.ref,
+                    isCollapsed: !!this.props.cell.state.isCollapsed
                 });
             }
         });
     }
 
     state = {
-        isCurrent: this.props.state.current === this.props.nodeRef,
-        isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed,
-        state: this.props.state,
-        // updaterCollapsed: true
+        isCurrent: this.props.cell.parent.current === this.ref,
+        isCollapsed: !!this.props.cell.state.isCollapsed
     }
 
-    static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) {
-        if (props.state === state.state) return null;
-        return {
-            isCurrent: props.state.current === props.nodeRef,
-            isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed,
-            state: props.state,
-            updaterCollapsed: true
-        };
+    static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null {
+        const isCurrent = props.cell.parent.current === props.cell.transform.ref;
+        const isCollapsed = !!props.cell.state.isCollapsed;
+
+        if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null;
+        return { isCurrent, isCollapsed };
     }
 
     setCurrent = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
         e.currentTarget.blur();
-        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
     }
 
     remove = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true });
+        PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true });
     }
 
     toggleVisible = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     toggleExpanded = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     highlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
     clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
         e.preventDefault();
-        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef });
+        PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref });
         e.currentTarget.blur();
     }
 
@@ -201,12 +199,11 @@ class StateTreeNodeLabel extends PluginUIComponent<
     // }
 
     render() {
-        const n = this.props.state.transforms.get(this.props.nodeRef)!;
-        const cell = this.props.state.cells.get(this.props.nodeRef);
+        const cell = this.props.cell;
+        const n = cell.transform;
         if (!cell) return null;
 
-        const isCurrent = this.is(this.props.state.behaviors.currentObject.value);
-
+        const isCurrent = this.state.isCurrent; // this.is(cell.parent.behaviors.currentObject.value);
 
         let label: any;
         if (cell.status === 'pending' || cell.status === 'processing') {
@@ -226,8 +223,8 @@ class StateTreeNodeLabel extends PluginUIComponent<
             }
         }
 
-        const children = this.props.state.tree.children.get(this.props.nodeRef);
-        const cellState = this.props.state.cellStates.get(this.props.nodeRef);
+        const children = cell.parent.tree.children.get(this.ref);
+        const cellState = cell.state;
 
         const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}>
             <span className='msp-icon msp-icon-visual-visibility' />
@@ -244,7 +241,7 @@ class StateTreeNodeLabel extends PluginUIComponent<
             {children.size > 0 &&  <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'>
                 <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} />
             </button>}
-            {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
+            {!cell.state.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'>
                 <span className='msp-icon msp-icon-remove' />
             </button>}{visibility}
         </div>;

+ 2 - 0
src/mol-state/object.ts

@@ -56,6 +56,8 @@ namespace StateObject {
 }
 
 interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> {
+    parent: State,
+
     transform: F,
 
     // Which object was used as a parent to create data in this cell

+ 2 - 0
src/mol-state/state.ts

@@ -206,6 +206,7 @@ class State {
         const root = tree.root;
 
         (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
+            parent: this,
             transform: root,
             sourceRef: void 0,
             obj: rootObject,
@@ -442,6 +443,7 @@ function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: Ini
     }
 
     const cell: StateObjectCell = {
+        parent: ctx.parent,
         transform,
         sourceRef: void 0,
         status: 'pending',

+ 4 - 0
src/mol-state/state/selection.ts

@@ -49,6 +49,7 @@ namespace StateSelection {
         parent(): Builder<C>;
         first(): Builder<C>;
         filter(p: (n: C) => boolean): Builder<C>;
+        withTag(tag: string): Builder<C>;
         withTransformer<T extends StateTransformer<any, StateObjectCell.Obj<C>, any>>(t: T): Builder<StateObjectCell<StateObjectCell.Obj<C>, StateTransform<T>>>;
         withStatus(s: StateObjectCell.Status): Builder<C>;
         subtree(): Builder;
@@ -200,6 +201,9 @@ namespace StateSelection {
     registerModifier('withStatus', withStatus);
     export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); }
 
+    registerModifier('withTag', withTag);
+    export function withTag(b: Selector, tag: string) { return filter(b, n => !!n.transform.tags && n.transform.tags.indexOf(tag) >= 0); }
+
     registerModifier('subtree', subtree);
     export function subtree(b: Selector) {
         return flatMap(b, (n, s) => {

+ 5 - 0
src/mol-state/transform.ts

@@ -102,6 +102,11 @@ namespace Transform {
         return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, state });
     }
 
+    export function hasTag(t: Transform, tag: string) {
+        if (!t.tags) return false;
+        return t.tags.indexOf(tag) >= 0;
+    }
+
     export interface Serialized {
         parent: string,
         transformer: string,