Browse Source

custom props are now included by default
+ structure parent helper now takes decorators into account
+ ui & api tweaks

David Sehnal 5 years ago
parent
commit
23b24bbb6c

+ 0 - 1
src/apps/viewer/index.ts

@@ -88,7 +88,6 @@ async function tryLoadFromUrl(ctx: PluginContext) {
                     format: format as any,
                     isBinary,
                     options: params.source.params.options,
-                    structure: params.source.params.structure,
                 }
             }
         }));

+ 58 - 52
src/mol-plugin-state/actions/structure.ts

@@ -19,6 +19,7 @@ import { Download, ParsePsf } from '../transforms/data';
 import { CoordinatesFromDcd, CustomModelProperties, CustomStructureProperties, TopologyFromPsf, TrajectoryFromModelAndCoordinates } from '../transforms/model';
 import { DataFormatProvider, guessCifVariant } from './data-format';
 import { applyTrajectoryHierarchyPreset } from '../builder/structure/hierarchy-preset';
+import { PresetStructureReprentations } from '../builder/structure/representation-preset';
 
 // TODO make unitcell creation part of preset
 
@@ -36,7 +37,8 @@ export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | P
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('mmCIF default builder', async () => {
             const trajectory = await ctx.builders.structure.parseTrajectory(data, 'mmcif');
-            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, noRepresentation: !options.visuals });
+            const representationPreset = options.visuals ? 'auto' : 'empty';
+            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, representationPreset });
         })
     }
 }
@@ -52,7 +54,8 @@ export const PdbProvider: DataFormatProvider<any> = {
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('PDB default builder', async () => {
             const trajectory = await ctx.builders.structure.parseTrajectory(data, 'pdb');
-            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, noRepresentation: !options.visuals });
+            const representationPreset = options.visuals ? 'auto' : 'empty';
+            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, representationPreset });
         })
     }
 }
@@ -68,7 +71,8 @@ export const GroProvider: DataFormatProvider<any> = {
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('GRO default builder', async () => {
             const trajectory = await ctx.builders.structure.parseTrajectory(data, 'gro');
-            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, noRepresentation: !options.visuals });
+            const representationPreset = options.visuals ? 'auto' : 'empty';
+            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, representationPreset });
         })
     }
 }
@@ -84,7 +88,8 @@ export const Provider3dg: DataFormatProvider<any> = {
     getDefaultBuilder: (ctx: PluginContext, data, options) => {
         return Task.create('3DG default builder', async () => {
             const trajectory = await ctx.builders.structure.parseTrajectory(data, '3dg');
-            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, noRepresentation: !options.visuals });
+            const representationPreset = options.visuals ? 'auto' : 'empty';
+            await applyTrajectoryHierarchyPreset(ctx, trajectory, 'first-model', { showUnitcell: options.visuals, representationPreset });
         })
     }
 }
@@ -123,59 +128,51 @@ export const DcdProvider: DataFormatProvider<any> = {
 
 //
 
-const DownloadModelRepresentationOptions = PD.Group({
+const DownloadModelRepresentationOptions = (plugin: PluginContext) => PD.Group({
     type: RootStructureDefinition.getParams(void 0, 'assembly').type,
-    noRepresentation: PD.Optional(PD.Boolean(false, { description: 'Omit creating default representation.' }))
-}, { isExpanded: false });
-
-const DownloadStructurePdbIdSourceOptions = PD.Group({
-    // supportProps: PD.Optional(PD.Boolean(false)),
+    representation: PD.Select(PresetStructureReprentations.auto.id,
+        plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name] as any),
+        { description: 'Which representation preset to use.' }),
     asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' }))
-});
+}, { isExpanded: false });
 
 export { DownloadStructure };
 type DownloadStructure = typeof DownloadStructure
 const DownloadStructure = StateAction.build({
     from: PluginStateObject.Root,
     display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its representation.' },
-    params: {
-        source: PD.MappedStatic('bcif-static', {
-            'pdbe-updated': PD.Group({
-                id: PD.Text('1cbs', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
-                structure: DownloadModelRepresentationOptions,
-                options: DownloadStructurePdbIdSourceOptions
-            }, { isFlat: true, label: 'PDBe Updated' }),
-            'rcsb': PD.Group({
-                id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
-                structure: DownloadModelRepresentationOptions,
-                options: DownloadStructurePdbIdSourceOptions
-            }, { isFlat: true, label: 'RCSB' }),
-            'pdb-dev': PD.Group({
-                id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma separated ids.' }),
-                structure: DownloadModelRepresentationOptions,
-                options: DownloadStructurePdbIdSourceOptions
-            }, { isFlat: true, label: 'PDBDEV' }),
-            'bcif-static': PD.Group({
-                id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
-                structure: DownloadModelRepresentationOptions,
-                options: DownloadStructurePdbIdSourceOptions
-            }, { isFlat: true, label: 'BinaryCIF (static PDBe Updated)' }),
-            'swissmodel': PD.Group({
-                id: PD.Text('Q9Y2I8', { label: 'UniProtKB AC(s)', description: 'One or more comma separated ACs.' }),
-                structure: DownloadModelRepresentationOptions,
-                options: DownloadStructurePdbIdSourceOptions
-            }, { isFlat: true, label: 'SWISS-MODEL', description: 'Loads the best homology model or experimental structure' }),
-            'url': PD.Group({
-                url: PD.Text(''),
-                format: PD.Select('mmcif', [['mmcif', 'CIF'], ['pdb', 'PDB']] as ['mmcif' | 'pdb', string][]),
-                isBinary: PD.Boolean(false),
-                structure: DownloadModelRepresentationOptions,
-                options: PD.Group({
-                    // supportProps: PD.Optional(PD.Boolean(false)),
-                    createRepresentation: PD.Optional(PD.Boolean(true))
-                })
-            }, { isFlat: true, label: 'URL' })
-        })
+    params: (_, plugin: PluginContext) => {
+        const options = DownloadModelRepresentationOptions(plugin);
+        return {
+            source: PD.MappedStatic('bcif-static', {
+                'pdbe-updated': PD.Group({
+                    id: PD.Text('1cbs', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
+                    options
+                }, { isFlat: true, label: 'PDBe Updated' }),
+                'rcsb': PD.Group({
+                    id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
+                    options
+                }, { isFlat: true, label: 'RCSB' }),
+                'pdb-dev': PD.Group({
+                    id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma separated ids.' }),
+                    options
+                }, { isFlat: true, label: 'PDBDEV' }),
+                'bcif-static': PD.Group({
+                    id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma separated PDB ids.' }),
+                    options
+                }, { isFlat: true, label: 'BinaryCIF (static PDBe Updated)' }),
+                'swissmodel': PD.Group({
+                    id: PD.Text('Q9Y2I8', { label: 'UniProtKB AC(s)', description: 'One or more comma separated ACs.' }),
+                    options
+                }, { isFlat: true, label: 'SWISS-MODEL', description: 'Loads the best homology model or experimental structure' }),
+                'url': PD.Group({
+                    url: PD.Text(''),
+                    format: PD.Select('mmcif', [['mmcif', 'CIF'], ['pdb', 'PDB']] as ['mmcif' | 'pdb', string][]),
+                    isBinary: PD.Boolean(false),
+                    options
+                }, { isFlat: true, label: 'URL' })
+            })
+        }
     }
 })(({ params, state }, plugin: PluginContext) => Task.create('Download Structure', async ctx => {
     plugin.behaviors.layout.leftPanelTabName.next('data');
@@ -227,7 +224,8 @@ const DownloadStructure = StateAction.build({
         default: throw new Error(`${(src as any).name} not supported.`);
     }
 
-    const createRepr = !params.source.params.structure.noRepresentation;
+    const representationPreset: any = params.source.params.options.representation || PresetStructureReprentations.auto.id;
+    const showUnitcell = representationPreset !== PresetStructureReprentations.empty.id;
 
     await state.transaction(async () => {
         if (downloadParams.length > 0 && asTrajectory) {
@@ -237,13 +235,21 @@ const DownloadStructure = StateAction.build({
             }, { state: { isGhost: true } });
             const trajectory = await plugin.builders.structure.parseTrajectory(blob, { formats: downloadParams.map((_, i) => ({ id: '' + i, format: 'cif' as 'cif' })) });
 
-            await applyTrajectoryHierarchyPreset(plugin, trajectory, 'first-model', { showUnitcell: createRepr, noRepresentation: !createRepr });
+            await applyTrajectoryHierarchyPreset(plugin, trajectory, 'first-model', {
+                structure: src.params.options.type,
+                showUnitcell,
+                representationPreset
+            });
         } else {
             for (const download of downloadParams) {
                 const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
                 const trajectory = await plugin.builders.structure.parseTrajectory(data, format);
 
-                await applyTrajectoryHierarchyPreset(plugin, trajectory, 'first-model', { showUnitcell: createRepr, noRepresentation: !createRepr });
+                await applyTrajectoryHierarchyPreset(plugin, trajectory, 'first-model', {
+                    structure: src.params.options.type,
+                    showUnitcell,
+                    representationPreset
+                });
             }
         }
     }).runInContext(ctx);

+ 11 - 16
src/mol-plugin-state/builder/structure/hierarchy-preset.ts

@@ -32,7 +32,6 @@ const FirstModelParams = (a: PluginStateObject.Molecule.Trajectory | undefined,
     model: PD.Optional(PD.Group(StateTransformer.getParamDefinition(StateTransforms.Model.ModelFromTrajectory, a, plugin))),
     showUnitcell: PD.Optional(PD.Boolean(true)),
     structure: PD.Optional(RootStructureDefinition.getParams(void 0, 'assembly').type),
-    noRepresentation: PD.Optional(PD.Boolean(false)),
     ...CommonParams(a, plugin)
 });
 
@@ -44,27 +43,20 @@ const firstModel = TrajectoryHierarchyPresetProvider({
         const builder = plugin.builders.structure;
 
         const model = await builder.createModel(trajectory, params.model);
-        const modelProperties = !!params.modelProperties
-            ? await builder.insertModelProperties(model, params.modelProperties) : void 0;
-        const modelChildParent = modelProperties || model;
+        const modelProperties = await builder.insertModelProperties(model, params.modelProperties);
 
         const structure = await builder.createStructure(modelProperties || model, params.structure);
-        const structureProperties = !!params.structureProperties
-            ? await builder.insertStructureProperties(structure, params.structureProperties) : void 0;
-        const structureChildParent = structureProperties || structure;
+        const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties);
 
-        const unitcell = params.showUnitcell === void 0 || !!params.showUnitcell ? await builder.tryCreateUnitcell(modelChildParent, undefined, { isHidden: true }) : void 0;
-
-        const representation = !params.noRepresentation
-            ? await plugin.builders.structure.representation.applyPreset(structureChildParent, params.representationPreset || 'auto')
-            : void 0;
+        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');
 
         return {
-            model: modelChildParent,
+            model: modelProperties,
             modelRoot: model,
             modelProperties,
             unitcell,
-            structure: structureChildParent,
+            structure: structureProperties,
             structureRoot: structure,
             structureProperties,
             representation
@@ -86,10 +78,13 @@ const allModels = TrajectoryHierarchyPresetProvider({
 
         for (let i = 0; i < tr.length; i++) {
             const model = await builder.createModel(trajectory, { modelIndex: i }, { isCollapsed: true });
-            const structure = await builder.createStructure(model, { name: 'deposited', params: {} });
+            const modelProperties = await builder.insertModelProperties(model, params.modelProperties);
+            const structure = await builder.createStructure(modelProperties || model, { name: 'deposited', params: {} });
+            const structureProperties = await builder.insertStructureProperties(structure, params.structureProperties);
+
             models.push(model);
             structures.push(structure);
-            await builder.representation.applyPreset(structure, params.representationPreset || 'auto', { globalThemeName: 'model-index' });
+            await builder.representation.applyPreset(structureProperties, params.representationPreset || 'auto', { globalThemeName: 'model-index' });
         }
 
         return { models, structures };

+ 9 - 0
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -67,6 +67,14 @@ function reprBuilder(plugin: PluginContext, params: CommonStructureRepresentatio
     return { update, builder, color, typeParams };
 }
 
+const empty = StructureRepresentationPresetProvider({
+    id: 'preset-structure-representation-empty',
+    display: { name: 'Empty', group: 'Preset' },
+    async apply(ref, params, plugin) {
+        return { };
+    }
+});
+
 const polymerAndLigand = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-polymer-and-ligand',
     display: { name: 'Polymer & Ligand', group: 'Preset' },
@@ -228,6 +236,7 @@ export function presetSelectionComponent(plugin: PluginContext, structure: State
 }
 
 export const PresetStructureReprentations = {
+    empty,
     auto,
     'atomic-detail': atomicDetail,
     'polymer-cartoon': polymerCartoon,

+ 9 - 0
src/mol-plugin-state/builder/structure/representation.ts

@@ -54,6 +54,15 @@ export class StructureRepresentationBuilder {
         return ret;
     }
 
+    getPresetSelect(s?: PluginStateObject.Molecule.Structure): PD.Select<string> {
+        const options: [string, string][] = [];
+        for (const p of this._providers) {
+            if (s && p.isApplicable && !p.isApplicable(s, this.plugin)) continue;
+            options.push([p.id, p.display.name]);
+        }
+        return PD.Select('auto', options);
+    }
+
     getPresetsWithOptions(s: PluginStateObject.Molecule.Structure) {
         const options: [string, string][] = [];
         const map: { [K in string]: PD.Any } = Object.create(null);

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

@@ -212,24 +212,41 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
         return this.plugin.updateDataState(update, { canUndo: 'Update Representation' });
     }
 
-    updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> {
-        if (components.length === 0) return Promise.resolve();
+    /**
+     * To update theme for all selected structures, use
+     *   plugin.dataTransaction(async () => {
+     *      for (const s of structure.hierarchy.selection.structures) await updateRepresentationsTheme(s.componets, ...);
+     *   }, { canUndo: 'Update Theme' });
+     */
+    updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> | undefined
+    updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: (c: StructureComponentRef, r: StructureRepresentationRef) => StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> | undefined
+    updateRepresentationsTheme(components: ReadonlyArray<StructureComponentRef>, paramsOrProvider: StructureComponentManager.UpdateThemeParams<any, any> | ((c: StructureComponentRef, r: StructureRepresentationRef) => StructureComponentManager.UpdateThemeParams<any, any>)) {
+        if (components.length === 0) return;
 
         const update = this.dataState.build();
 
         for (const c of components) {
             for (const repr of c.representations) {
                 const old = repr.cell.transform.params;
-                const colorTheme = params.color
-                    ? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.color, params.colorParams)
-                    : void 0;
-                const sizeTheme = params.color
-                    ? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.size, params.sizeParams)
-                    : void 0;
-                update.to(repr.cell).update(prev => {
-                    if (colorTheme) prev.colorTheme = colorTheme;
-                    if (sizeTheme) prev.sizeTheme = sizeTheme;
-                });
+                const params: StructureComponentManager.UpdateThemeParams<any, any> = typeof paramsOrProvider === 'function' ? paramsOrProvider(c, repr) : paramsOrProvider;
+
+                const colorTheme = params.color === 'default'
+                    ? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name)
+                    : params.color
+                        ? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.color, params.colorParams)
+                        : void 0;
+                const sizeTheme = params.size === 'default'
+                    ? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name)
+                    : params.color
+                        ? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.size, params.sizeParams)
+                        : void 0;
+
+                if (colorTheme || sizeTheme) {
+                    update.to(repr.cell).update(prev => {
+                        if (colorTheme) prev.colorTheme = colorTheme;
+                        if (sizeTheme) prev.sizeTheme = sizeTheme;
+                    });
+                }
             }
         }
 
@@ -392,9 +409,9 @@ namespace StructureComponentManager {
         /**
          * this works for any theme name (use 'name as any'), but code completion will break
          */
-        color?: C,
+        color?: C | 'default',
         colorParams?: ColorTheme.BuiltInParams<C>,
-        size?: S,
+        size?: S | 'default',
         sizeParams?: SizeTheme.BuiltInParams<S>
     }
 }

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

@@ -229,7 +229,7 @@ export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
 //         const actions = cell.status === 'ok' && <StateObjectActionSelect state={current.state} nodeRef={ref} plugin={this.plugin} />
 
 //         if (cell.status === 'error') {
-//             return <>            
+//             return <>
 //                 <SectionHeader icon='flow-cascade' title={`${cell.obj?.label || transform.transformer.definition.display.name}`} desc={transform.transformer.definition.display.name} />
 //                 <UpdateTransformControl state={current.state} transform={transform} customHeader='none' />
 //                 {actions}
@@ -249,7 +249,7 @@ export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> {
 //             </ExpandGroup>);
 //         }
 
-//         return <>            
+//         return <>
 //             <SectionHeader icon='flow-cascade' title={`${parent.obj?.label || parent.transform.transformer.definition.display.name}`} desc={parent.transform.transformer.definition.display.name} />
 //             <UpdateTransformControl state={current.state} transform={parent.transform} customHeader='none' />
 //             {decorators && <div className='msp-controls-section'>{decorators}</div>}

+ 16 - 29
src/mol-plugin-ui/state/tree.tsx

@@ -14,6 +14,7 @@ import { ActionMenu } from '../controls/action-menu';
 import { ApplyActionControl } from './apply-action';
 import { ControlGroup } from '../controls/common';
 import { UpdateTransformControl } from './update-transform';
+import { StateTreeSpine } from '../../mol-state/tree/spine';
 
 export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> {
     state = { showActions: true };
@@ -254,6 +255,19 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
         (item?.value as any)();
     }
 
+    updates() {
+        const cell = this.props.cell;
+        const decoratorChain = StateTreeSpine.getDecoratorChain(cell.parent, cell.transform.ref);
+
+        const decorators = [];
+        for (let i = decoratorChain.length - 1; i >= 0; i--) {
+            const d = decoratorChain[i];
+            decorators!.push(<UpdateTransformControl key={`${d.transform.transformer.id}-${i}`} state={cell.parent} transform={d.transform} noMargin wrapInExpander />);
+        }
+
+        return decorators;
+    }
+
     render() {
         const cell = this.props.cell;
         const n = cell.transform;
@@ -315,38 +329,11 @@ class StateTreeNodeLabel extends PluginUIComponent<{ cell: StateObjectCell, dept
             let actions = this.actions;
             return <div style={{ marginBottom: '1px' }}>
                 {row}
-                <UpdateTransformControl state={cell.parent} transform={cell.transform} noMargin wrapInExpander />
+                {this.updates()}
                 {actions && <ActionMenu items={actions} onSelect={this.selectAction} />}
             </div>
         }
 
-        // if (this.state.isCurrent) {
-        //     return <>
-        //         {row}
-        //         <StateTreeNodeTransform {...this.props} toggleCollapsed={this.toggleUpdaterObs} />
-        //     </>
-        // }
-
         return row;
     }
-}
-
-// class StateTreeNodeTransform extends PluginUIComponent<{ nodeRef: string, state: State, depth: number, toggleCollapsed?: Observable<any> }> {
-//     componentDidMount() {
-//         // this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => {
-//         //     if (this.props.nodeRef !== ref || this.props.state !== state) return;
-//         //     this.forceUpdate();
-//         // });
-//     }
-
-//     render() {
-//         const ref = this.props.nodeRef;
-//         const cell = this.props.state.cells.get(ref)!;
-//         const parent: StateObjectCell | undefined = (cell.sourceRef && this.props.state.cells.get(cell.sourceRef)!) || void 0;
-
-//         if (!parent || parent.status !== 'ok') return null;
-
-//         const transform = cell.transform;
-//         return <UpdateTransformContol state={this.props.state} transform={transform} initiallyCollapsed={true} toggleCollapsed={this.props.toggleCollapsed} />;
-//     }
-// }
+}

+ 1 - 3
src/mol-plugin-ui/structure/components.tsx

@@ -81,9 +81,7 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
     }
 
     get presetActions() {
-        const actions = [
-            ActionMenu.Item('Clear', null),
-        ];
+        const actions = [];
         const pivot = this.plugin.managers.structure.component.pivotStructure;
         const providers = this.plugin.builders.structure.representation.getPresets(pivot?.cell.obj)
         for (const p of providers) {

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

@@ -55,8 +55,6 @@ const StructureRepresentationInteractionParams = (plugin: PluginContext) => {
 
 type StructureRepresentationInteractionProps = PD.ValuesFor<ReturnType<typeof StructureRepresentationInteractionParams>>
 
-//PD.Values<typeof StructureRepresentationInteractionParams>
-
 export enum StructureRepresentationInteractionTags {
     Group = 'structure-interaction-group',
     ResidueSel = 'structure-interaction-residue-sel',
@@ -213,7 +211,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W
         const state = this.plugin.state.data;
         const builder = state.build();
 
-        const all = StateSelection.Generators.root.subtree();        
+        const all = StateSelection.Generators.root.subtree();
         for (const repr of state.select(all.withTag(StructureRepresentationInteractionTags.ResidueRepr))) {
             builder.to(repr).update(this.params.focusParams);
         }

+ 66 - 11
src/mol-plugin/util/substructure-parent-helper.ts

@@ -5,39 +5,91 @@
  */
 
 import { Structure } from '../../mol-model/structure';
-import { State, StateObject, StateSelection, StateObjectCell } from '../../mol-state';
+import { State, StateObject, StateSelection, StateObjectCell, StateTransform } from '../../mol-state';
 import { PluginContext } from '../context';
 import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { arraySetAdd, arraySetRemove } from '../../mol-util/array';
 
 export { SubstructureParentHelper };
 
 class SubstructureParentHelper {
+    private decorators = new Map<string, string[]>();
     private root = new Map<Structure, { ref: string, count: number }>();
     private tracked = new Map<string, Structure>();
 
-    /** Returns the root node of given structure if existing */
-    get(s: Structure): StateObjectCell<PluginStateObject.Molecule.Structure> | undefined {
+    /** Returns the root node of given structure if existing, takes decorators into account */
+    get(s: Structure, ignoreDecorators = false): StateObjectCell<PluginStateObject.Molecule.Structure> | undefined {
         const r = this.root.get(s);
         if (!r) return;
-        return this.plugin.state.data.cells.get(r.ref);
+        const decorators = this.decorators.get(r.ref);
+        if (ignoreDecorators || !decorators) return this.plugin.state.data.cells.get(r.ref);
+        return this.plugin.state.data.cells.get(this.findDeepestDecorator(r.ref, decorators));
+    }
+
+    private findDeepestDecorator(ref: string, decorators: string[]) {
+        if (decorators.length === 0) return ref;
+        if (decorators.length === 1) return decorators[0];
+
+        const cells = this.plugin.state.data.cells;
+        let depth = 0, ret = ref;
+        for (const dr of decorators) {
+            let c = cells.get(dr);
+            let d = 0;
+            while (c && c.transform.ref !== StateTransform.RootRef) {
+                d++;
+                c = cells.get(c.transform.parent);
+            }
+            if (d > depth) {
+                ret = dr;
+                depth = d;
+            }
+        }
+        return ret;
+    }
+
+    private addDecorator(root: string, ref: string) {
+        if (this.decorators.has(root)) {
+            arraySetAdd(this.decorators.get(root)!, ref);
+        } else {
+            this.decorators.set(root, [ref]);
+        }
+    }
+
+    private tryRemoveDecorator(root: string, ref: string) {
+        if (this.decorators.has(root)) {
+            const xs = this.decorators.get(root)!;
+            arraySetRemove(xs, ref);
+            if (xs.length === 0) this.decorators.delete(root);
+        }
     }
 
     private addMapping(state: State, ref: string, obj: StateObject) {
         if (!PluginStateObject.Molecule.Structure.is(obj)) return;
-        const parent = state.select(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
 
         this.tracked.set(ref, obj.data);
 
+        let parentRef;
         // if the structure is already present in the tree, do not rewrite the root.
         if (this.root.has(obj.data)) {
-            this.root.get(obj.data)!.count++;
-            return;
+            const e = this.root.get(obj.data)!;
+            parentRef = e.ref;
+            e.count++;
+        } else {
+            const parent = state.select(StateSelection.Generators.byRef(ref).rootOfType([PluginStateObject.Molecule.Structure]))[0];
+
+            if (!parent) {
+                this.root.set(obj.data, { ref, count: 1 });
+            } else {
+                parentRef = parent.transform.ref;
+                this.root.set(obj.data, { ref: parentRef, count: 1 });
+            }
         }
 
-        if (!parent) {
-            this.root.set(obj.data, { ref, count : 1 });
-        } else {
-            this.root.set(obj.data, { ref: parent.transform.ref, count: 1 });
+        if (!parentRef) return;
+
+        const cell = state.cells.get(ref);
+        if (cell?.transform.isDecorator) {
+            this.addDecorator(parentRef, ref);
         }
     }
 
@@ -48,6 +100,9 @@ class SubstructureParentHelper {
         this.tracked.delete(ref);
 
         const root = this.root.get(s)!;
+
+        this.tryRemoveDecorator(root.ref, ref);
+
         if (root.count > 1) {
             root.count--;
         } else {