From ebf74e6580aa211e2fa384c5bd8b2998f5e72e5d Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Sat, 9 Mar 2019 21:16:13 +0100
Subject: [PATCH] mol-state: better typing for cells and selections

---
 src/apps/basic-wrapper/index.ts               |  6 +-
 .../structure-representation-interaction.ts   |  2 +-
 .../skin/base/components/transformer.scss     |  1 +
 src/mol-plugin/skin/base/icons.scss           |  6 +-
 src/mol-plugin/state/actions/structure.ts     | 19 ++---
 src/mol-plugin/state/animation/built-in.ts    | 76 ++++++++-----------
 src/mol-plugin/ui/controls.tsx                |  3 +-
 src/mol-plugin/ui/state.tsx                   |  2 +-
 src/mol-state/action.ts                       |  2 +-
 src/mol-state/object.ts                       |  9 ++-
 src/mol-state/state.ts                        |  2 +-
 src/mol-state/state/builder.ts                | 17 +++--
 src/mol-state/state/selection.ts              | 29 ++++++-
 src/mol-state/transform.ts                    | 10 +--
 src/mol-state/transformer.ts                  |  6 +-
 15 files changed, 106 insertions(+), 84 deletions(-)

diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts
index 383b8736c..aaa87f3d9 100644
--- a/src/apps/basic-wrapper/index.ts
+++ b/src/apps/basic-wrapper/index.ts
@@ -11,7 +11,7 @@ import { PluginCommands } from 'mol-plugin/command';
 import { StateTransforms } from 'mol-plugin/state/transforms';
 import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
 import { Color } from 'mol-util/color';
-import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/objects';
+import { PluginStateObject as PSO } from 'mol-plugin/state/objects';
 import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
 import { StateBuilder } from 'mol-state';
 import { StripedResidues } from './coloring';
@@ -123,12 +123,12 @@ class BasicWrapper {
         applyStripes: async () => {
             const state = this.plugin.state.dataState;
 
-            const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
+            const visuals = state.selectQ(q => q.ofTransformer(StateTransforms.Representation.StructureRepresentation3D));
             const tree = state.build();
             const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues };
 
             for (const v of visuals) {
-                tree.to(v.transform.ref).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
+                tree.to(v).update(old => ({ ...old, colorTheme }));
             }
 
             await PluginCommands.State.Update.dispatch(this.plugin, { state, tree });
diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
index 959baa33b..497d14205 100644
--- a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
+++ b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts
@@ -55,7 +55,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
         const refs = StateSelection.findUniqueTagsInSubtree(tree, cell.transform.ref, TagSet);
 
         if (!refs['structure-interaction-group']) {
-            refs['structure-interaction-group'] = builder.to(cell.transform.ref).group(StateTransforms.Misc.CreateGroup,
+            refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup,
                 { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref;
         }
 
diff --git a/src/mol-plugin/skin/base/components/transformer.scss b/src/mol-plugin/skin/base/components/transformer.scss
index f5316f080..8fe043e01 100644
--- a/src/mol-plugin/skin/base/components/transformer.scss
+++ b/src/mol-plugin/skin/base/components/transformer.scss
@@ -97,6 +97,7 @@
     left: 0;
     top: 0;
     width: $row-height;
+    padding: 0;
 }
 
 .msp-transform-default-params:hover {
diff --git a/src/mol-plugin/skin/base/icons.scss b/src/mol-plugin/skin/base/icons.scss
index c0d183795..2c4c0590c 100644
--- a/src/mol-plugin/skin/base/icons.scss
+++ b/src/mol-plugin/skin/base/icons.scss
@@ -208,4 +208,8 @@
 
 .msp-icon-code:before {
 	content: "\e834";
-}
\ No newline at end of file
+}
+
+.msp-icon-floppy:before {
+	content: "\e8d0";
+}
diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts
index 52284f8b2..76b4b0d9f 100644
--- a/src/mol-plugin/state/actions/structure.ts
+++ b/src/mol-plugin/state/actions/structure.ts
@@ -245,27 +245,24 @@ export const UpdateTrajectory = StateAction.build({
         by: PD.asOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 }))
     }
 })(({ params, state }) => {
-    const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-        .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+    const models = state.selectQ(q => q.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
 
     const update = state.build();
 
     if (params.action === 'reset') {
         for (const m of models) {
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                () => ({ modelIndex: 0 }));
+            update.to(m).update({ modelIndex: 0 });
         }
     } else {
         for (const m of models) {
             const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
             if (!parent || !parent.obj) continue;
-            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                old => {
-                    let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
-                    if (modelIndex < 0) modelIndex += traj.data.length;
-                    return { modelIndex };
-                });
+            const traj = parent.obj;
+            update.to(m).update(old => {
+                let modelIndex = (old.modelIndex + params.by!) % traj.data.length;
+                if (modelIndex < 0) modelIndex += traj.data.length;
+                return { modelIndex };
+            });
         }
     }
 
diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts
index 123fcbfc1..f8211ea44 100644
--- a/src/mol-plugin/state/animation/built-in.ts
+++ b/src/mol-plugin/state/animation/built-in.ts
@@ -30,8 +30,7 @@ export const AnimateModelIndex = PluginStateAnimation.create({
         }
 
         const state = ctx.plugin.state.dataState;
-        const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-            .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+        const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
 
         if (models.length === 0) {
             // nothing more to do here
@@ -47,37 +46,36 @@ export const AnimateModelIndex = PluginStateAnimation.create({
         for (const m of models) {
             const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
             if (!parent || !parent.obj) continue;
-            const traj = parent.obj as PluginStateObject.Molecule.Trajectory;
-            update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory,
-                old => {
-                    const len = traj.data.length;
-                    if (len !== 1) {
-                        allSingles = false;
-                    } else {
+            const traj = parent.obj;
+            update.to(m).update(old => {
+                const len = traj.data.length;
+                if (len !== 1) {
+                    allSingles = false;
+                } else {
+                    return old;
+                }
+                let dir: -1 | 1 = 1;
+                if (params.mode.name === 'once') {
+                    dir = params.mode.params.direction === 'backward' ? -1 : 1;
+                    // if we are at start or end already, do nothing.
+                    if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
+                        isEnd = true;
                         return old;
                     }
-                    let dir: -1 | 1 = 1;
-                    if (params.mode.name === 'once') {
-                        dir = params.mode.params.direction === 'backward' ? -1 : 1;
-                        // if we are at start or end already, do nothing.
-                        if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) {
-                            isEnd = true;
-                            return old;
-                        }
-                    } else if (params.mode.name === 'palindrome') {
-                        if (old.modelIndex === 0) dir = 1;
-                        else if (old.modelIndex === len - 1) dir = -1;
-                        else dir = palindromeDirections[m.transform.ref] || 1;
-                    }
-                    palindromeDirections[m.transform.ref] = dir;
+                } else if (params.mode.name === 'palindrome') {
+                    if (old.modelIndex === 0) dir = 1;
+                    else if (old.modelIndex === len - 1) dir = -1;
+                    else dir = palindromeDirections[m.transform.ref] || 1;
+                }
+                palindromeDirections[m.transform.ref] = dir;
 
-                    let modelIndex = (old.modelIndex + dir) % len;
-                    if (modelIndex < 0) modelIndex += len;
+                let modelIndex = (old.modelIndex + dir) % len;
+                if (modelIndex < 0) modelIndex += len;
 
-                    isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
+                isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1);
 
-                    return { modelIndex };
-                });
+                return { modelIndex };
+            });
         }
 
         await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
@@ -102,13 +100,11 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
         const update = state.build();
         let changed = false;
         for (const r of reprs) {
-            const unwinds = state.select(StateSelection.Generators.byValue(r)
-                .children()
-                .filter(c => c.transform.transformer === StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D));
+            const unwinds = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, r.transform.ref));
             if (unwinds.length > 0) continue;
 
             changed = true;
-            update.to(r.transform.ref)
+            update.to(r)
                 .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } });
         }
 
@@ -128,11 +124,9 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.dataState;
-        const anims = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.transformer === StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D));
+        const anims = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D));
 
         if (anims.length === 0) {
-            // nothing more to do here
             return { kind: 'finished' };
         }
 
@@ -142,7 +136,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({
         const newTime = (animState.t + d) % 1;
 
         for (const m of anims) {
-            update.to(m.transform.ref).update(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, _ => ({ t: newTime }));
+            update.to(m).update({ t: newTime });
         }
 
         await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
@@ -165,9 +159,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
         const update = state.build();
         let changed = false;
         for (const r of reprs) {
-            const explodes = state.select(StateSelection.Generators.byValue(r)
-                .children()
-                .filter(c => c.transform.transformer === StateTransforms.Representation.ExplodeStructureRepresentation3D));
+            const explodes = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.ExplodeStructureRepresentation3D, r.transform.ref));
             if (explodes.length > 0) continue;
 
             changed = true;
@@ -191,11 +183,9 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
     },
     async apply(animState, t, ctx) {
         const state = ctx.plugin.state.dataState;
-        const anims = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure.Representation3DState)
-            .filter(c => c.transform.transformer === StateTransforms.Representation.ExplodeStructureRepresentation3D));
+        const anims = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.ExplodeStructureRepresentation3D));
 
         if (anims.length === 0) {
-            // nothing more to do here
             return { kind: 'finished' };
         }
 
@@ -205,7 +195,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({
         const newTime = (animState.t + d) % 1;
 
         for (const m of anims) {
-            update.to(m.transform.ref).update(StateTransforms.Representation.ExplodeStructureRepresentation3D, _ => ({ t: newTime }));
+            update.to(m).update({ t: newTime });
         }
 
         await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } });
diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx
index 0bc8ee24e..7985f1754 100644
--- a/src/mol-plugin/ui/controls.tsx
+++ b/src/mol-plugin/ui/controls.tsx
@@ -22,8 +22,7 @@ export class TrajectoryViewportControls extends PluginUIComponent<{}, { show: bo
     private update = () => {
         const state = this.plugin.state.dataState;
 
-        const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model)
-            .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory));
+        const models = state.selectQ(q => q.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
 
         if (models.length === 0) {
             this.setState({ show: false });
diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx
index eeba3b574..31265bb4d 100644
--- a/src/mol-plugin/ui/state.tsx
+++ b/src/mol-plugin/ui/state.tsx
@@ -93,7 +93,7 @@ class LocalStateSnapshots extends PluginUIComponent<
             }}/>
 
             <div className='msp-btn-row-group'>
-                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='record' /> Save</button>
+                <button className='msp-btn msp-btn-block msp-form-control' onClick={this.add}><Icon name='floppy' /> Save</button>
                 {/* <button className='msp-btn msp-btn-block msp-form-control' onClick={this.upload} disabled={this.state.isUploading}>Upload</button> */}
                 <button className='msp-btn msp-btn-block msp-form-control' onClick={this.clear}>Clear</button>
             </div>
diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts
index 31b597fc2..81baf5fc5 100644
--- a/src/mol-state/action.ts
+++ b/src/mol-state/action.ts
@@ -46,7 +46,7 @@ namespace StateAction {
         run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>,
 
         /** Test if the transform can be applied to a given node */
-        isApplicable?(a: A, aTransform: StateTransform<any, A, any>, globalCtx: unknown): boolean
+        isApplicable?(a: A, aTransform: StateTransform<StateTransformer<any, A, any>>, globalCtx: unknown): boolean
     }
 
     export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> {
diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts
index 612964518..b74efcf5e 100644
--- a/src/mol-state/object.ts
+++ b/src/mol-state/object.ts
@@ -8,7 +8,7 @@ import { UUID } from 'mol-util';
 import { StateTransform } from './transform';
 import { ParamDefinition } from 'mol-util/param-definition';
 import { State } from './state';
-import { StateSelection } from 'mol-state';
+import { StateSelection, StateTransformer } from 'mol-state';
 
 export { StateObject, StateObjectCell }
 
@@ -55,8 +55,8 @@ namespace StateObject {
     };
 }
 
-interface StateObjectCell<T = StateObject> {
-    transform: StateTransform,
+interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> {
+    transform: F,
 
     // Which object was used as a parent to create data in this cell
     sourceRef: StateTransform.Ref | undefined,
@@ -77,6 +77,9 @@ interface StateObjectCell<T = StateObject> {
 namespace StateObjectCell {
     export type Status = 'ok' | 'error' | 'pending' | 'processing'
 
+    export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never
+    export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never
+
     export interface State {
         isHidden: boolean,
         isCollapsed: boolean
diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts
index c343b2aba..6c70a4071 100644
--- a/src/mol-state/state.ts
+++ b/src/mol-state/state.ts
@@ -103,7 +103,7 @@ class State {
      * Select Cells by building a query generated on the fly.
      * @example state.select(q => q.byRef('test').subtree())
      */
-    selectQ(selector: (q: typeof StateSelection.Generators) => StateSelection.Selector) {
+    selectQ<C extends StateObjectCell>(selector: (q: typeof StateSelection.Generators) => StateSelection.Selector<C>) {
         if (typeof selector === 'string') return StateSelection.select(selector, this);
         return StateSelection.select(selector(StateSelection.Generators), this)
     }
diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts
index d1e3f9d63..46ea7bdaf 100644
--- a/src/mol-state/state/builder.ts
+++ b/src/mol-state/state/builder.ts
@@ -42,7 +42,12 @@ namespace StateBuilder {
         private state: BuildState;
         get editInfo() { return this.state.editInfo; }
 
-        to<A extends StateObject>(ref: StateTransform.Ref) { return new To<A>(this.state, ref, this); }
+        to<A extends StateObject>(ref: StateTransform.Ref): To<A>
+        to<C extends StateObjectCell>(cell: C): To<StateObjectCell.Obj<C>, StateTransform.Transformer<StateObjectCell.Transform<C>>>
+        to(refOrCell: StateTransform.Ref | StateObjectCell) {
+            const ref = typeof refOrCell === 'string' ? refOrCell : refOrCell.transform.ref;
+            return new To<StateObject, StateTransformer>(this.state, ref, this);
+        }
         toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); }
         delete(ref: StateTransform.Ref) {
             this.editInfo.count++;
@@ -53,7 +58,7 @@ namespace StateBuilder {
         constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } }
     }
 
-    export class To<A extends StateObject> implements StateBuilder {
+    export class To<A extends StateObject, T extends StateTransformer = StateTransformer> implements StateBuilder {
         get editInfo() { return this.state.editInfo; }
 
         readonly ref: StateTransform.Ref;
@@ -121,14 +126,16 @@ namespace StateBuilder {
         }
 
         update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>): Root
-        update<T extends StateTransformer<any, A, any> = StateTransformer<any, A, any>>(params: StateTransformer.Params<T>): Root
-        update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) {
+        update(params: StateTransformer.Params<T> | ((old: StateTransformer.Params<T>) => StateTransformer.Params<T>)): Root
+        update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T | any, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) {
             let params: any;
             if (provider) {
                 const old = this.state.tree.transforms.get(this.ref)!;
                 params = provider(old.params as any);
             } else {
-                params = paramsOrTransformer;
+                params = typeof paramsOrTransformer === 'function'
+                    ? paramsOrTransformer(this.state.tree.transforms.get(this.ref)!.params)
+                    : paramsOrTransformer;
             }
 
             if (this.state.tree.setParams(this.ref, params)) {
diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts
index 3bea867d5..12a319073 100644
--- a/src/mol-state/state/selection.ts
+++ b/src/mol-state/state/selection.ts
@@ -8,6 +8,7 @@ import { StateObject, StateObjectCell } from '../object';
 import { State } from '../state';
 import { StateTree } from '../tree';
 import { StateTransform } from '../transform';
+import { StateTransformer } from '../transformer';
 
 namespace StateSelection {
     export type Selector<C extends StateObjectCell = StateObjectCell> = Query<C> | Builder<C> | string | C;
@@ -48,6 +49,7 @@ namespace StateSelection {
         parent(): Builder<C>;
         first(): Builder<C>;
         filter(p: (n: C) => boolean): 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;
         children(): Builder;
@@ -89,18 +91,26 @@ namespace StateSelection {
 
         export function byValue<T extends StateObjectCell>(...objects: T[]) { return build(() => (state: State) => objects); }
 
-        export function rootsOfType<T extends StateObject.Ctor>(type: T) {
+        export function rootsOfType<T extends StateObject.Ctor>(type: T, root: StateTransform.Ref = StateTransform.RootRef) {
             return build(() => state => {
                 const ctx = { roots: [] as StateObjectCell<StateObject.From<T>>[], cells: state.cells, type: type.type };
-                StateTree.doPreOrder(state.tree, state.tree.root, ctx, _findRootsOfType);
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findRootsOfType);
                 return ctx.roots;
             });
         }
 
-        export function ofType<T extends StateObject.Ctor>(type: T) {
+        export function ofType<T extends StateObject.Ctor>(type: T, root: StateTransform.Ref = StateTransform.RootRef) {
             return build(() => state => {
                 const ctx = { ret: [] as StateObjectCell<StateObject.From<T>>[], cells: state.cells, type: type.type };
-                StateTree.doPreOrder(state.tree, state.tree.root, ctx, _findOfType);
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findOfType);
+                return ctx.ret;
+            });
+        }
+
+        export function ofTransformer<T extends StateTransformer<any, A, any>, A extends StateObject>(t: T, root: StateTransform.Ref = StateTransform.RootRef) {
+            return build(() => state => {
+                const ctx = { ret: [] as StateObjectCell<A, StateTransform<T>>[], cells: state.cells, t };
+                StateTree.doPreOrder(state.tree, state.tree.transforms.get(root), ctx, _findOfTransformer);
                 return ctx.ret;
             });
         }
@@ -121,6 +131,14 @@ namespace StateSelection {
             }
             return true;
         }
+
+        function _findOfTransformer(n: StateTransform, _: any, s: { t: StateTransformer, ret: StateObjectCell[], cells: State.Cells }) {
+            const cell = s.cells.get(n.ref);
+            if (cell && cell.obj && cell.transform.transformer === s.t) {
+                s.ret.push(cell);
+            }
+            return true;
+        }
     }
 
     registerModifier('flatMap', flatMap);
@@ -206,6 +224,9 @@ namespace StateSelection {
     registerModifier('ancestorOfType', ancestorOfType);
     export function ancestorOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapObject(b, (n, s) => findAncestorOfType(s.tree, s.cells, n.transform.ref, types))); }
 
+    registerModifier('withTransformer', withTransformer);
+    export function withTransformer(b: Selector, t: StateTransformer) { return filter(b, o => o.transform.transformer === t); }
+
     registerModifier('rootOfType', rootOfType);
     export function rootOfType(b: Selector, types: StateObject.Ctor[]) { return unique(mapObject(b, (n, s) => findRootOfType(s.tree, s.cells, n.transform.ref, types))); }
 
diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts
index 12de75d1c..9703a2f53 100644
--- a/src/mol-state/transform.ts
+++ b/src/mol-state/transform.ts
@@ -4,23 +4,23 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { StateObject } from './object';
 import { StateTransformer } from './transformer';
 import { UUID } from 'mol-util';
 
 export { Transform as StateTransform }
 
-interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
+interface Transform<T extends StateTransformer = StateTransformer> {
     readonly parent: Transform.Ref,
-    readonly transformer: StateTransformer<A, B, P>,
+    readonly transformer: T,
     readonly props: Transform.Props,
     readonly ref: Transform.Ref,
-    readonly params?: P,
+    readonly params?: StateTransformer.Params<T>,
     readonly version: string
 }
 
 namespace Transform {
     export type Ref = string
+    export type Transformer<T extends Transform> = T extends Transform<infer S> ? S : never
 
     export const RootRef = '-=root=-' as Ref;
 
@@ -36,7 +36,7 @@ namespace Transform {
         props?: Props
     }
 
-    export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: StateTransformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> {
+    export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> {
         const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref;
         return {
             parent,
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index a7ba452d4..01759e738 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -14,8 +14,8 @@ import { StateTreeSpine } from './tree/spine';
 
 export { Transformer as StateTransformer }
 
-interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> {
-    apply(parent: StateTransform.Ref, params?: P, props?: Partial<StateTransform.Options>): StateTransform<A, B, P>,
+interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = any> {
+    apply(parent: StateTransform.Ref, params?: P, props?: Partial<StateTransform.Options>): StateTransform<this>,
     toAction(): StateAction<A, void, P>,
     readonly namespace: string,
     readonly id: Transformer.Id,
@@ -136,7 +136,7 @@ namespace Transformer {
         }
 
         const t: Transformer<A, B, P> = {
-            apply(parent, params, props) { return StateTransform.create<A, B, P>(parent, t, params, props); },
+            apply(parent, params, props) { return StateTransform.create<Transformer<A, B, P>>(parent, t, params, props); },
             toAction() { return StateAction.fromTransformer(t); },
             namespace,
             id,
-- 
GitLab