Browse Source

Merge branch 'master' into wboit

Alexander Rose 4 years ago
parent
commit
c5ca51fd80

+ 10 - 6
src/apps/viewer/index.ts

@@ -35,6 +35,7 @@ import { PluginStateObject } from '../../mol-plugin-state/objects';
 import { StateTransforms } from '../../mol-plugin-state/transforms';
 import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
 import { Mp4Export } from '../../extensions/mp4-export';
+import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
 
 require('mol-plugin-ui/skin/light.scss');
 
@@ -149,7 +150,7 @@ export class Viewer {
         return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
     }
 
-    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) {
+    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
         const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
             source: {
@@ -158,7 +159,7 @@ export class Viewer {
                     url: Asset.Url(url),
                     format: format as any,
                     isBinary,
-                    options: params.source.params.options,
+                    options: { ...params.source.params.options, representationParams: options?.representationParams as any },
                 }
             }
         }));
@@ -179,7 +180,7 @@ export class Viewer {
         await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
     }
 
-    loadPdb(pdb: string) {
+    loadPdb(pdb: string, options?: LoadStructureOptions) {
         const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
         const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
@@ -193,7 +194,7 @@ export class Viewer {
                             params: PdbDownloadProvider[provider].defaultValue as any
                         }
                     },
-                    options: params.source.params.options,
+                    options: { ...params.source.params.options, representationParams: options?.representationParams as any },
                 }
             }
         }));
@@ -264,10 +265,13 @@ export class Viewer {
     }
 }
 
+export interface LoadStructureOptions {
+    representationParams?: StructureRepresentationPresetProvider.CommonParams
+}
+
 export interface VolumeIsovalueInfo {
     type: 'absolute' | 'relative',
     value: number,
     color: Color,
     alpha?: number
-}
-
+}

+ 1 - 1
src/mol-canvas3d/canvas3d.ts

@@ -147,7 +147,7 @@ const cancelAnimationFrame = typeof window !== 'undefined'
 namespace Canvas3D {
     export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
-    export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, position?: Vec3 }
+    export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
     export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number, enableWboit: boolean }> = {}) {
         const gl = getGLContext(canvas, {

+ 2 - 4
src/mol-canvas3d/helper/interaction-events.ts

@@ -71,14 +71,12 @@ export class Canvas3dInteractionHelper {
 
         if (e === InputEvent.Click) {
             const loci = this.getLoci(this.id);
-            this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, position: this.position });
+            this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
             this.prevLoci = loci;
             return;
         }
 
-        if (!this.inside || this.currentIdentifyT !== t || !xyChanged) {
-            return;
-        }
+        if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
 
         const loci = this.getLoci(this.id);
         this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });

+ 5 - 0
src/mol-model-formats/structure/basic/entities.ts

@@ -90,6 +90,11 @@ export function getEntities(data: BasicData, properties: Model['properties']): E
         assignSubtype = true;
     }
 
+    if (entityIds.size < subtypes.length) {
+        // still unassigned subtypes, need to derive from component id/type
+        assignSubtype = true;
+    }
+
     if (assignSubtype) {
         const chemCompType = new Map<string, string>();
         if (data.chem_comp) {

+ 7 - 4
src/mol-plugin-state/actions/structure.ts

@@ -9,7 +9,7 @@ import { PluginContext } from '../../mol-plugin/context';
 import { StateAction, StateSelection, StateTransformer } from '../../mol-state';
 import { Task } from '../../mol-task';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { PresetStructureRepresentations } from '../builder/structure/representation-preset';
+import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../builder/structure/representation-preset';
 import { BuiltInTrajectoryFormat, BuiltInTrajectoryFormats } from '../formats/trajectory';
 import { RootStructureDefinition } from '../helpers/root-structure';
 import { PluginStateObject } from '../objects';
@@ -24,6 +24,7 @@ const DownloadModelRepresentationOptions = (plugin: PluginContext) => PD.Group({
     representation: PD.Select(PresetStructureRepresentations.auto.id,
         plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name, p.display.group] as any),
         { description: 'Which representation preset to use.' }),
+    representationParams: PD.Group(StructureRepresentationPresetProvider.CommonParams, { isHidden: true }),
     asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' }))
 }, { isExpanded: false });
 
@@ -144,7 +145,8 @@ const DownloadStructure = StateAction.build({
             await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default', {
                 structure,
                 showUnitcell,
-                representationPreset
+                representationPreset,
+                representationPresetParams: params.source.params.options.representationParams
             });
         } else {
             for (const download of downloadParams) {
@@ -154,7 +156,8 @@ const DownloadStructure = StateAction.build({
                 await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default', {
                     structure,
                     showUnitcell,
-                    representationPreset
+                    representationPreset,
+                    representationPresetParams: params.source.params.options.representationParams
                 });
             }
         }
@@ -187,7 +190,7 @@ export const UpdateTrajectory = StateAction.build({
         }
     } else {
         for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, PluginStateObject.Molecule.Trajectory);
             if (!parent || !parent.obj) continue;
             const traj = parent.obj;
             update.to(m).update(old => {

+ 2 - 2
src/mol-plugin-state/animation/built-in/model-index.ts

@@ -26,7 +26,7 @@ export const AnimateModelIndex = PluginStateAnimation.create({
         const state = ctx.state.data;
         const models = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Model.ModelFromTrajectory));
         for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, PluginStateObject.Molecule.Trajectory);
             if (parent && parent.obj && parent.obj.data.frameCount > 1) return { canApply: true };
         }
         return { canApply: false, reason: 'No trajectory to animate' };
@@ -53,7 +53,7 @@ export const AnimateModelIndex = PluginStateAnimation.create({
         let isEnd = false, allSingles = true;
 
         for (const m of models) {
-            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]);
+            const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, PluginStateObject.Molecule.Trajectory);
             if (!parent || !parent.obj) continue;
             const traj = parent.obj;
             if (traj.data.frameCount <= 1) continue;

+ 4 - 3
src/mol-plugin-state/builder/structure/hierarchy-preset.ts

@@ -11,7 +11,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { StateObjectRef, StateTransformer } from '../../../mol-state';
 import { StateTransforms } from '../../transforms';
 import { RootStructureDefinition } from '../../helpers/root-structure';
-import { PresetStructureRepresentations } from './representation-preset';
+import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from './representation-preset';
 import { PluginContext } from '../../../mol-plugin/context';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { Model } from '../../../mol-model/structure';
@@ -27,7 +27,7 @@ export namespace TrajectoryHierarchyPresetProvider {
     export const CommonParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
         modelProperties: PD.Optional(PD.Group(StateTransformer.getParamDefinition(StateTransforms.Model.CustomModelProperties, void 0, plugin))),
         structureProperties: PD.Optional(PD.Group(StateTransformer.getParamDefinition(StateTransforms.Model.CustomStructureProperties, void 0, plugin))),
-        representationPreset: PD.Optional(PD.Text<keyof PresetStructureRepresentations>('auto' as const)),
+        representationPreset: PD.Optional(PD.Text<keyof PresetStructureRepresentations>('auto' as const))
     });
 }
 
@@ -37,6 +37,7 @@ const DefaultParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plu
     model: PD.Optional(PD.Group(StateTransformer.getParamDefinition(StateTransforms.Model.ModelFromTrajectory, a, plugin))),
     showUnitcell: PD.Optional(PD.Boolean(false)),
     structure: PD.Optional(RootStructureDefinition.getParams(void 0, 'assembly').type),
+    representationPresetParams: PD.Optional(PD.Group(StructureRepresentationPresetProvider.CommonParams)),
     ...CommonParams(a, plugin)
 });
 
@@ -60,7 +61,7 @@ const defaultPreset = TrajectoryHierarchyPresetProvider({
         const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties);
 
         const unitcell = params.showUnitcell === void 0 || !!params.showUnitcell ? await builder.tryCreateUnitcell(modelProperties, undefined, { isHidden: true }) : void 0;
-        const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto');
+        const representation = await plugin.builders.structure.representation.applyPreset(structureProperties, params.representationPreset || 'auto', params.representationPresetParams);
 
         return {
             model,

+ 23 - 12
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -37,6 +37,7 @@ export namespace StructureRepresentationPresetProvider {
         theme: PD.Optional(PD.Group({
             globalName: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
             carbonColor: PD.Optional(PD.Select('chain-id', PD.arrayToOptions(['chain-id', 'operator-name', 'element-symbol'] as const))),
+            symmetryColor: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
             focus: PD.Optional(PD.Group({
                 name: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
                 params: PD.Optional(PD.Value<ColorTheme.BuiltInParams<ColorTheme.BuiltIn>>({} as any))
@@ -53,7 +54,11 @@ export namespace StructureRepresentationPresetProvider {
                 : { name, params: {} };
     }
 
-    export function reprBuilder(plugin: PluginContext, params: CommonParams) {
+    function isSymmetry(structure: Structure) {
+        return structure.units.some(u => !u.conformation.operator.assembly && u.conformation.operator.spgrOp >= 0);
+    }
+
+    export function reprBuilder(plugin: PluginContext, params: CommonParams, structure?: Structure) {
         const update = plugin.state.data.build();
         const builder = plugin.builders.structure.representation;
         const typeParams = {
@@ -66,8 +71,11 @@ export namespace StructureRepresentationPresetProvider {
         const ballAndStickColor: ColorTheme.BuiltInParams<'element-symbol'> = params.theme?.carbonColor !== undefined
             ? { carbonColor: getCarbonColorParams(params.theme?.carbonColor) }
             : { };
+        const symmetryColor: ColorTheme.BuiltIn | undefined = structure && params.theme?.symmetryColor
+            ? isSymmetry(structure) ? params.theme?.symmetryColor : color
+            : color;
 
-        return { update, builder, color, typeParams, ballAndStickColor };
+        return { update, builder, color, symmetryColor, typeParams, ballAndStickColor };
     }
 
     export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
@@ -156,10 +164,10 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
         const waterType = (components.water?.obj?.data?.elementCount || 0) > 50_000 ? 'line' : 'ball-and-stick';
         const lipidType = (components.lipid?.obj?.data?.elementCount || 0) > 20_000 ? 'line' : 'ball-and-stick';
 
-        const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params);
+        const { update, builder, typeParams, color, symmetryColor, ballAndStickColor } = reprBuilder(plugin, params, structure);
 
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'polymer' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'ligand' }),
             nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'non-standard' }),
             branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color, colorParams: ballAndStickColor }, { tag: 'branched-ball-and-stick' }),
@@ -202,10 +210,11 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
             smoothness: structure.isCoarseGrained ? 0.5 : 1.5,
         };
 
-        const { update, builder, typeParams, color } = reprBuilder(plugin, params);
+        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+
         const representations = {
-            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'protein' }),
-            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color }, { tag: 'nucleic' })
+            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'protein' }),
+            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'nucleic' })
         };
 
         await update.commit({ revertOnError: true });
@@ -252,9 +261,10 @@ const coarseSurface = StructureRepresentationPresetProvider({
             });
         }
 
-        const { update, builder, typeParams, color } = reprBuilder(plugin, params);
+        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color }, { tag: 'polymer' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'polymer' })
         };
 
         await update.commit({ revertOnError: true });
@@ -284,9 +294,10 @@ const polymerCartoon = StructureRepresentationPresetProvider({
             sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
         };
 
-        const { update, builder, typeParams, color } = reprBuilder(plugin, params);
+        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color }, { tag: 'polymer' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' })
         };
 
         await update.commit({ revertOnError: true });
@@ -331,7 +342,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
             });
         }
 
-        const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params);
+        const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
         const colorParams = lowResidueElementRatio
             ? { carbonColor: { name: 'element-symbol', params: {} } }
             : ballAndStickColor;

+ 1 - 1
src/mol-plugin-state/manager/interactivity.ts

@@ -72,7 +72,7 @@ namespace InteractivityManager {
 
     export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
-    export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, position?: Vec3 }
+    export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
     export type LociMarkProvider = (loci: Representation.Loci, action: MarkerAction) => void
 

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

@@ -255,7 +255,7 @@ export namespace VolumeStreaming {
         }
 
         private getStructureRoot() {
-            return this.plugin.state.data.select(StateSelection.Generators.byRef(this.ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+            return this.plugin.state.data.select(StateSelection.Generators.byRef(this.ref).rootOfType(PluginStateObject.Molecule.Structure))[0];
         }
 
         register(ref: string): void {

+ 1 - 1
src/mol-state/state.ts

@@ -150,7 +150,7 @@ class State {
 
     /**
      * Select Cells using the provided selector.
-     * @example state.query(StateSelection.Generators.byRef('test').ancestorOfType([type]))
+     * @example state.query(StateSelection.Generators.byRef('test').ancestorOfType(type))
      * @example state.query('test')
      */
     select<C extends StateObjectCell>(selector: StateSelection.Selector<C>) {

+ 57 - 23
src/mol-state/state/selection.ts

@@ -55,8 +55,13 @@ namespace StateSelection {
         subtree(): Builder;
         children(): Builder;
         ofType<T extends StateObject.Ctor>(t: T): Builder<StateObjectCell<StateObject.From<T>>>;
-        ancestorOfType<T extends StateObject.Ctor>(t: T[]): Builder<StateObjectCell<StateObject.From<T>>>;
-        rootOfType(t: StateObject.Ctor[]): Builder;
+
+        ancestor<T extends StateObject.Ctor>(test: (c: StateObjectCell) => (boolean | void | undefined)): Builder<StateObjectCell<StateObject.From<T>>>;
+        ancestorOfType<T extends StateObject.Ctor>(t: T | T[]): Builder<StateObjectCell<StateObject.From<T>>>;
+        ancestorWithTransformer<T extends StateTransformer>(transfomers: T | T[]): Builder<StateObjectCell<StateTransformer.To<T>>>;
+
+        root<T extends StateObject.Ctor>(test: (c: StateObjectCell) => (boolean | void | undefined)): Builder<StateObjectCell<StateObject.From<T>>>;
+        rootOfType<T extends StateObject.Ctor>(t: T | T[]): Builder<StateObjectCell<StateObject.From<T>>>;
 
         select(state: State): CellSeq<C>
     }
@@ -241,50 +246,79 @@ namespace StateSelection {
     registerModifier('ofType', ofType);
     export function ofType(b: Selector, t: StateObject.Ctor) { return filter(b, n => n.obj ? n.obj.type === t.type : false); }
 
+    registerModifier('ancestor', ancestor);
+    export function ancestor(b: Selector, test: (c: StateObjectCell) => (boolean | void | undefined)) { return unique(mapObject(b, (n, s) => findAncestor(s.tree, s.cells, n.transform.ref, test))); }
+
     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('ancestorWithTransformer', ancestorWithTransformer);
+    export function ancestorWithTransformer(b: Selector, transfomers: StateTransformer[]) { return unique(mapObject(b, (n, s) => findAncestorWithTransformer(s.tree, s.cells, n.transform.ref, transfomers))); }
+
     registerModifier('withTransformer', withTransformer);
     export function withTransformer(b: Selector, t: StateTransformer) { return filter(b, o => o.transform.transformer === t); }
 
+    registerModifier('root', root);
+    export function root(b: Selector, test: (c: StateObjectCell) => (boolean | void | undefined)) { return unique(mapObject(b, (n, s) => findRoot(s.tree, s.cells, n.transform.ref, test))); }
+
     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))); }
+    export function rootOfType(b: Selector, types: StateObject.Ctor | StateObject.Ctor[]) { return unique(mapObject(b, (n, s) => findRootOfType(s.tree, s.cells, n.transform.ref, types))); }
 
     registerModifier('parent', parent);
     export function parent(b: Selector) { return unique(mapObject(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); }
 
-    export function findAncestorOfType<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: T[]): StateObjectCell<StateObject.From<T>> | undefined {
-        let current = tree.transforms.get(root)!, len = types.length;
+    function _findAncestor<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, test: (c: StateObjectCell) => (boolean | void | undefined), findClosest: boolean) {
+        let current = tree.transforms.get(root)!;
+        let ret: StateObjectCell<StateObject.From<T>> | undefined = void 0;
         while (true) {
             current = tree.transforms.get(current.parent)!;
             const cell = cells.get(current.ref)!;
-            if (!cell.obj) return void 0;
-            const obj = cell.obj;
-            for (let i = 0; i < len; i++) {
-                if (obj.type === types[i].type) return cell as StateObjectCell<StateObject.From<T>>;
+            if (cell.obj && test(cell)) {
+                ret = cell as any;
+                if (findClosest) return ret;
             }
             if (current.ref === StateTransform.RootRef) {
-                return void 0;
+                return ret;
             }
         }
     }
 
-    export function findRootOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined {
-        let parent: StateObjectCell | undefined, _root = root;
-        while (true) {
-            const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types);
-            if (_parent) {
-                parent = _parent;
-                _root = _parent.transform.ref;
-            } else {
-                break;
-            }
-        }
-        return parent;
+    // Return first ancestor that satisfies the given test
+    export function findAncestor<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, test: (c: StateObjectCell) => (boolean | void | undefined)) {
+        return _findAncestor<T>(tree, cells, root, test, true);
+    }
+
+    // Return last (with lowest depth) ancestor that satisfies the given test
+    export function findRoot<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, test: (c: StateObjectCell) => (boolean | void | undefined)) {
+        return _findAncestor<T>(tree, cells, root, test, false);
+    }
+
+    export function findAncestorWithTransformer<T extends StateTransformer>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, transfomers: T | T[]): StateObjectCell<StateTransformer.To<T>> | undefined {
+        return findAncestor<StateObject.Ctor<StateTransformer.To<T>>>(tree, cells, root, Array.isArray(transfomers)
+            ? cell => transfomers.indexOf(cell.transform.transformer as any) >= 0
+            : cell => cell.transform.transformer === transfomers
+        );
+    }
+
+    export function findAncestorOfType<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: T | T[]): StateObjectCell<StateObject.From<T>> | undefined {
+        return findAncestor<StateObject.Ctor<StateObject.From<T>>>(tree, cells, root, _testTypes(types));
+    }
+
+    export function findRootOfType<T extends StateObject.Ctor>(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: T | T[]): StateObjectCell<StateObject.From<T>> | undefined {
+        return findRoot<T>(tree, cells, root, _testTypes(types));
+    }
+
+    function _testTypes<T extends StateObject.Ctor>(types: T | T[]) {
+        return Array.isArray(types)
+            ? (cell: StateObjectCell) => {
+                for (const t of types) {
+                    if (t.type === cell.obj!.type) return true;
+                }
+            } : (cell: StateObjectCell) => cell.obj!.type === types.type;
     }
 
     export function findUniqueTagsInSubtree<K extends string = string>(tree: StateTree, root: StateTransform.Ref, tags: Set<K>): { [name in K]?: StateTransform.Ref } {
-        return StateTree.doPreOrder(tree, tree.transforms.get(root), { refs: { }, tags }, _findUniqueTagsInSubtree).refs;
+        return StateTree.doPreOrder(tree, tree.transforms.get(root), { refs: {}, tags }, _findUniqueTagsInSubtree).refs;
     }
 
     function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) {