Browse Source

mol-plugin-state: representation presets "sync" instead of recreate

David Sehnal 5 years ago
parent
commit
09fba43a1c

+ 1 - 1
src/apps/state-docs/pd-to-md.ts

@@ -39,7 +39,7 @@ function paramInfo(param: PD.Any, offset: number): string {
     }
 }
 
-function oToS(options: readonly (readonly [string, string] | readonly [string, string, string])[]) {
+function oToS(options: readonly (readonly [string, string] | readonly [string, string, string | undefined])[]) {
     return options.map(o => `'${o[0]}'`).join(', ');
 }
 

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

@@ -129,7 +129,7 @@ export const DcdProvider: DataFormatProvider<any> = {
 const DownloadModelRepresentationOptions = (plugin: PluginContext) => PD.Group({
     type: RootStructureDefinition.getParams(void 0, 'auto').type,
     representation: PD.Select(PresetStructureRepresentations.auto.id,
-        plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name] as any),
+        plugin.builders.structure.representation.getPresets().map(p => [p.id, p.display.name, p.display.group] as any),
         { description: 'Which representation preset to use.' }),
     asTrajectory: PD.Optional(PD.Boolean(false, { description: 'Load all entries into a single trajectory.' }))
 }, { isExpanded: false });

+ 1 - 1
src/mol-plugin-state/builder/preset-provider.ts

@@ -10,7 +10,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 
 export interface PresetProvider<O extends StateObject = StateObject, P = any, S = {}> {
     id: string,
-    display: { name: string, group: string, description?: string },
+    display: { name: string, group?: string, description?: string },
     isApplicable?(a: O, plugin: PluginContext): boolean,
     params?(a: O | undefined, plugin: PluginContext): PD.For<P>,
     apply(a: StateObjectRef<O>, params: P, plugin: PluginContext): Promise<S> | S

+ 2 - 2
src/mol-plugin-state/builder/structure.ts

@@ -118,7 +118,7 @@ export class StructureBuilder {
     }
 
     /** returns undefined if the component is empty/null */
-    async tryCreateComponent(structure: StateObjectRef<SO.Molecule.Structure>, params: StructureComponentParams, key: string, tags?: string[]): Promise<StateObjectRef<SO.Molecule.Structure> | undefined> {
+    async tryCreateComponent(structure: StateObjectRef<SO.Molecule.Structure>, params: StructureComponentParams, key: string, tags?: string[]): Promise<StateObjectSelector<SO.Molecule.Structure> | undefined> {
         const state = this.dataState;
 
         const root = state.build().to(structure);
@@ -157,7 +157,7 @@ export class StructureBuilder {
         }, key, params?.tags);
     }
 
-    tryCreateComponentFromSelection(structure: StateObjectRef<SO.Molecule.Structure>, selection: StructureSelectionQuery, key: string, params?: { label?: string, tags?: string[] }): Promise<StateObjectRef<SO.Molecule.Structure> | undefined> {
+    tryCreateComponentFromSelection(structure: StateObjectRef<SO.Molecule.Structure>, selection: StructureSelectionQuery, key: string, params?: { label?: string, tags?: string[] }): Promise<StateObjectSelector<SO.Molecule.Structure> | undefined> {
         return this.plugin.runTask(Task.create('Query Component', async taskCtx => {
             let { label, tags } = params || { };
             label = (label || '').trim();

+ 38 - 29
src/mol-plugin-state/builder/structure/representation-preset.ts

@@ -12,15 +12,19 @@ import { VisualQuality, VisualQualityOptions } from '../../../mol-geo/geometry/b
 import { ColorTheme } from '../../../mol-theme/color';
 import { Structure } from '../../../mol-model/structure';
 import { PluginContext } from '../../../mol-plugin/context';
-import { StateObjectRef } from '../../../mol-state';
+import { StateObjectRef, StateObjectSelector } from '../../../mol-state';
 import { StaticStructureComponentType } from '../../helpers/structure-component';
 import { StructureSelectionQueries as Q } from '../../helpers/structure-selection-query';
 
-export interface StructureRepresentationPresetProvider<P = any, S = {}> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
-export function StructureRepresentationPresetProvider<P, S>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
+export interface StructureRepresentationPresetProvider<P = any, S extends _Result = _Result> extends PresetProvider<PluginStateObject.Molecule.Structure, P, S> { }
+export function StructureRepresentationPresetProvider<P, S extends _Result>(repr: StructureRepresentationPresetProvider<P, S>) { return repr; }
 export namespace StructureRepresentationPresetProvider {
     export type Params<P extends StructureRepresentationPresetProvider> = P extends StructureRepresentationPresetProvider<infer T> ? T : never;
     export type State<P extends StructureRepresentationPresetProvider> = P extends StructureRepresentationPresetProvider<infer _, infer S> ? S : never;
+    export type Result = {
+        components?: { [name: string]: StateObjectSelector | undefined },
+        representations?: { [name: string]: StateObjectSelector | undefined }
+    }
 
     export const CommonParams = {
         ignoreHydrogens: PD.Optional(PD.Boolean(false)),
@@ -44,13 +48,15 @@ export namespace StructureRepresentationPresetProvider {
     }
 }
 
+type _Result = StructureRepresentationPresetProvider.Result
+
 const CommonParams = StructureRepresentationPresetProvider.CommonParams
 type CommonParams = StructureRepresentationPresetProvider.CommonParams
 const reprBuilder = StructureRepresentationPresetProvider.reprBuilder
 
 const auto = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-auto',
-    display: { name: 'Automatic', group: 'Preset' },
+    display: { name: 'Automatic' },
     params: () => CommonParams,
     apply(ref, params, plugin) {
         const structure = StateObjectRef.resolveAndCheck(plugin.state.data, ref)?.obj?.data;
@@ -73,15 +79,17 @@ const auto = StructureRepresentationPresetProvider({
 
 const empty = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-empty',
-    display: { name: 'Empty', group: 'Preset' },
+    display: { name: 'Empty' },
     async apply(ref, params, plugin) {
-        return { };
+        return {  };
     }
 });
 
+const BuiltInPresetGroupName = 'Basic'
+
 const polymerAndLigand = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-polymer-and-ligand',
-    display: { name: 'Polymer & Ligand', group: 'Preset' },
+    display: { name: 'Polymer & Ligand', group: BuiltInPresetGroupName },
     params: () => CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
@@ -98,15 +106,13 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }),
-            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color }),
-            nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color: color || 'polymer-id' }),
-            branched: components.branched && {
-                ballAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.15 }, color }),
-                snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color }),
-            },
-            water: builder.buildRepresentation(update, components.water, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.51 }, color }),
-            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'polymer-id' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }, { tag: 'polymer' }),
+            ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color }, { tag: 'ligand' }),
+            nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color: color || 'polymer-id' }, { tag: 'non-standard' }),
+            branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.15 }, color }, { tag: 'branched-ball-and-stick' }),
+            branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color }, { tag: 'branched-snfg-3d' }),
+            water: builder.buildRepresentation(update, components.water, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.51 }, color }, { tag: 'water' }),
+            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'polymer-id' }, { tag: 'coarse' })
         };
 
         await plugin.updateDataState(update, { revertOnError: false });
@@ -116,7 +122,7 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
 
 const proteinAndNucleic = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-protein-and-nucleic',
-    display: { name: 'Protein & Nucleic', group: 'Preset' },
+    display: { name: 'Protein & Nucleic', group: BuiltInPresetGroupName },
     params: () => CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
@@ -129,8 +135,8 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
 
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams, color }),
-            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams, color })
+            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams, color }, { tag: 'protein' }),
+            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams, color }, { tag: 'nucleic' })
         };
 
         await plugin.updateDataState(update, { revertOnError: true });
@@ -140,7 +146,7 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
 
 const coarseSurface = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-coarse-surface',
-    display: { name: 'Coarse Surface', group: 'Preset' },
+    display: { name: 'Coarse Surface', group: BuiltInPresetGroupName },
     params: () => CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
@@ -152,26 +158,29 @@ const coarseSurface = StructureRepresentationPresetProvider({
         const gaussianProps = Object.create(null);
         const components = Object.create(null);
 
+        let selectionType = 'polymer'
+
         if (size === Structure.Size.Gigantic) {
             Object.assign(gaussianProps, {
                 radiusOffset: 1,
                 smoothness: 0.5,
                 visuals: ['structure-gaussian-surface-mesh']
             })
+            selectionType = 'trace'
             components.trace = await presetSelectionComponent(plugin, structureCell, 'trace')
         } else if(size === Structure.Size.Huge) {
             Object.assign(gaussianProps, {
                 smoothness: 0.5,
             })
-            components.trace = await presetSelectionComponent(plugin, structureCell, 'polymer')
+            components.trace = await presetStaticComponent(plugin, structureCell, 'polymer')
         } else {
-            components.trace = await presetSelectionComponent(plugin, structureCell, 'polymer')
+            components.trace = await presetStaticComponent(plugin, structureCell, 'polymer')
         }
 
 
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            trace: builder.buildRepresentation(update, components.trace, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color })
+            trace: builder.buildRepresentation(update, components.trace, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color }, { tag: selectionType })
         };
 
         await plugin.updateDataState(update, { revertOnError: true });
@@ -181,19 +190,19 @@ const coarseSurface = StructureRepresentationPresetProvider({
 
 const polymerCartoon = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-polymer-cartoon',
-    display: { name: 'Polymer Cartoon', group: 'Preset' },
+    display: { name: 'Polymer Cartoon', group: BuiltInPresetGroupName },
     params: () => CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
         if (!structureCell) return {};
 
         const components = {
-            polymer: await presetSelectionComponent(plugin, structureCell, 'polymer'),
+            polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
         };
 
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams, color }, { tag: 'polymer' })
         };
 
         await plugin.updateDataState(update, { revertOnError: true });
@@ -203,19 +212,19 @@ const polymerCartoon = StructureRepresentationPresetProvider({
 
 const atomicDetail = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-atomic-detail',
-    display: { name: 'Atomic Detail', group: 'Preset' },
+    display: { name: 'Atomic Detail', group: BuiltInPresetGroupName },
     params: () => CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
         if (!structureCell) return {};
 
         const components = {
-            all: await presetSelectionComponent(plugin, structureCell, 'all'),
+            all: await presetStaticComponent(plugin, structureCell, 'all'),
         };
 
         const { update, builder, typeParams, color } = reprBuilder(plugin, params);
         const representations = {
-            all: builder.buildRepresentation(update, components.all, { type: 'ball-and-stick', typeParams, color })
+            all: builder.buildRepresentation(update, components.all, { type: 'ball-and-stick', typeParams, color }, { tag: 'all' })
         };
 
         await plugin.updateDataState(update, { revertOnError: true });

+ 8 - 7
src/mol-plugin-state/builder/structure/representation.ts

@@ -54,10 +54,10 @@ export class StructureRepresentationBuilder {
     }
 
     getPresetSelect(s?: PluginStateObject.Molecule.Structure): PD.Select<string> {
-        const options: [string, string][] = [];
+        const options: [string, string, string | undefined][] = [];
         for (const p of this._providers) {
             if (s && p.isApplicable && !p.isApplicable(s, this.plugin)) continue;
-            options.push([p.id, p.display.name]);
+            options.push([p.id, p.display.name, p.display.group]);
         }
         return PD.Select('auto', options);
     }
@@ -121,16 +121,17 @@ export class StructureRepresentationBuilder {
         return selector;
     }
 
-    async buildRepresentation<P extends StructureRepresentationBuiltInProps>(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: P, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>): Promise<StateObjectSelector<PluginStateObject.Molecule.Structure.Representation3D>>
-    async buildRepresentation<P extends StructureRepresentationProps>(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: P, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>): Promise<StateObjectSelector<PluginStateObject.Molecule.Structure.Representation3D>>
-    async buildRepresentation(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: any, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>) {
+    buildRepresentation<P extends StructureRepresentationBuiltInProps>(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: P, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>): StateObjectSelector<PluginStateObject.Molecule.Structure.Representation3D>
+    buildRepresentation<P extends StructureRepresentationProps>(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: P, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>): StateObjectSelector<PluginStateObject.Molecule.Structure.Representation3D>
+    buildRepresentation(builder: StateBuilder.Root, structure: StateObjectRef<PluginStateObject.Molecule.Structure> | undefined, props: any, options?: Partial<StructureRepresentationBuilder.AddRepresentationOptions>) {
         if (!structure) return;
         const data = StateObjectRef.resolveAndCheck(this.dataState, structure)?.obj?.data;
         if (!data) return;
 
         const params = createStructureRepresentationParams(this.plugin, data, props);
-        return builder.to(structure)
-            .apply(StructureRepresentation3D, params, { tags: options?.tag, state: options?.initialState }).selector;
+        return options?.tag
+            ? builder.to(structure).applyOrUpdateTagged(options.tag, StructureRepresentation3D, params, { state: options?.initialState }).selector
+            : builder.to(structure).apply(StructureRepresentation3D, params, { state: options?.initialState }).selector;
     }
 
     constructor(public plugin: PluginContext) {

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

@@ -25,6 +25,7 @@ import { HierarchyRef, StructureComponentRef, StructureRef, StructureRepresentat
 import { createStructureColorThemeParams, createStructureSizeThemeParams } from '../../helpers/structure-representation-params';
 import { ColorTheme } from '../../../mol-theme/color';
 import { SizeTheme } from '../../../mol-theme/size';
+import { objectForEach } from '../../../mol-util/object';
 
 export { StructureComponentManager };
 
@@ -103,15 +104,52 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
         }
     }
 
-    applyPreset<P = any, S = {}>(structures: ReadonlyArray<StructureRef>, provider: StructureRepresentationPresetProvider<P, S>, params?: P): Promise<any>  {
+    applyPreset<P extends StructureRepresentationPresetProvider>(structures: ReadonlyArray<StructureRef>, provider: P, params?: StructureRepresentationPresetProvider.Params<P>): Promise<any>  {
         return this.plugin.dataTransaction(async () => {
-            await this.clearComponents(structures);
             for (const s of structures) {
-                await this.plugin.builders.structure.representation.applyPreset(s.cell, provider, params);
+                const preset = await this.plugin.builders.structure.representation.applyPreset(s.cell, provider, params);
+                await this.syncPreset(s, preset);
             }
         }, { canUndo: 'Preset' });
     }
 
+    private async syncPreset(root: StructureRef, preset?: StructureRepresentationPresetProvider.Result) {
+        if (!preset || !preset.components) return this.clearComponents([root]);
+
+        const keptRefs = new Set<string>();
+        objectForEach(preset.components, c => {
+            if (c) keptRefs.add(c.ref);
+        });
+
+        if (preset.representations) {
+            objectForEach(preset.representations, r => {
+                if (r) keptRefs.add(r.ref);
+            });
+        }
+
+        if (keptRefs.size === 0) return this.clearComponents([root]);
+
+        let changed = false;
+        const update = this.dataState.build();
+
+        const sync = (r: HierarchyRef) => {
+            if (!keptRefs.has(r.cell.transform.ref)) {
+                changed = true;
+                update.delete(r.cell);
+            }
+        };
+
+        for (const c of root.components) {
+            sync(c);
+            for (const r of c.representations) sync(r);
+            if (c.genericRepresentations) {
+                for (const r of c.genericRepresentations) sync(r);
+            }
+        }
+
+        if (changed) return this.plugin.updateDataState(update);
+    }
+
     clear(structures: ReadonlyArray<StructureRef>) {
         return this.clearComponents(structures);
     }

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

@@ -90,13 +90,9 @@ class ComponentEditorControls extends PurePluginUIComponent<{}, ComponentEditorC
     }
 
     get presetActions() {
-        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) {
-            actions.push(ActionMenu.Item(p.display.name, p));
-        }
-        return actions;
+        const providers = this.plugin.builders.structure.representation.getPresets(pivot?.cell.obj);
+        return ActionMenu.createItems(providers, { label: p => p.display.name, category: p => p.display.group });
     }
 
     applyPreset: ActionMenu.OnSelect = item => {

+ 5 - 4
src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts

@@ -146,16 +146,17 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
 
 const assemblySymmetryPreset = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-rcsb-assembly-symmetry',
-    display: { name: 'Assembly Symmetry', group: 'Preset' },
+    display: { name: 'Assembly Symmetry', group: 'Annotation' },
     params: () => StructureRepresentationPresetProvider.CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
-        const model = structureCell?.obj?.data.model
+        const model = structureCell?.obj?.data.model;
         if (!structureCell || !model) return {};
 
-        await tryCreateAssemblySymmetry(plugin, structureCell)
+        const assemblySymmetry = await tryCreateAssemblySymmetry(plugin, structureCell);
+        const repr = await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: Tag.Cluster as any }, plugin);
 
-        return await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: Tag.Cluster as any }, plugin)
+        return { components: repr.components, representations: { ...repr.components, assemblySymmetry } };
     }
 });
 

+ 6 - 7
src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts

@@ -290,7 +290,7 @@ const hasClash = StructureSelectionQuery('Residues with Clashes', MS.struct.modi
 
 const validationReportPreset = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-rcsb-validation-report',
-    display: { name: 'Validation Report', group: 'Preset' },
+    display: { name: 'Validation Report', group: 'Annotation' },
     params: () => StructureRepresentationPresetProvider.CommonParams,
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
@@ -305,17 +305,16 @@ const validationReportPreset = StructureRepresentationPresetProvider({
 
         const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params, globalThemeName: colorTheme }, plugin)
 
-        components.clashes = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, hasClash.expression, 'clashes', { label: 'Clashes' })
+        const clashes = await plugin.builders.structure.tryCreateComponentFromExpression(structureCell, hasClash.expression, 'clashes', { label: 'Clashes' })
 
         const { update, builder, typeParams, color } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
+        let clashesBallAndStick, clashesSnfg3d;
         if (representations) {
-            (representations as any).clashes = components.clashes && {
-                ballAndStick: builder.buildRepresentation(update, components.clashes, { type: 'ball-and-stick', typeParams, color: colorTheme }),
-                snfg3d: builder.buildRepresentation<any>(update, components.clashes, { type: ClashesRepresentationProvider.name, typeParams, color }),
-            }
+            clashesBallAndStick = builder.buildRepresentation(update, components.clashes, { type: 'ball-and-stick', typeParams, color: colorTheme }, { tag: 'clashes-ball-and-stick' });
+            clashesSnfg3d = builder.buildRepresentation<any>(update, components.clashes, { type: ClashesRepresentationProvider.name, typeParams, color }, { tag: 'clashes-snfg-3d' });
         }
 
         await plugin.updateDataState(update, { revertOnError: false });
-        return { components, representations };
+        return { components: { ...components, clashes }, representations: { ...representations, clashesBallAndStick, clashesSnfg3d } };
     }
 });

+ 3 - 3
src/mol-util/param-definition.ts

@@ -74,10 +74,10 @@ export namespace ParamDefinition {
     export interface Select<T> extends Base<T> {
         type: 'select'
         /** array of (value, label) tuples */
-        options: readonly (readonly [T, string] | readonly [T, string, string])[]
+        options: readonly (readonly [T, string] | readonly [T, string, string | undefined])[]
         cycle?: boolean
     }
-    export function Select<T>(defaultValue: T, options: readonly (readonly [T, string] | readonly [T, string, string])[], info?: Info & { cycle?: boolean }): Select<T> {
+    export function Select<T>(defaultValue: T, options: readonly (readonly [T, string] | readonly [T, string, string | undefined])[], info?: Info & { cycle?: boolean }): Select<T> {
         return setInfo<Select<T>>({ type: 'select', defaultValue: checkDefaultKey(defaultValue, options), options, cycle: info?.cycle }, info)
     }
 
@@ -469,7 +469,7 @@ export namespace ParamDefinition {
         return ret;
     }
 
-    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string] | readonly [T, string, string])[]) {
+    function checkDefaultKey<T>(k: T, options: readonly (readonly [T, string] | readonly [T, string, string | undefined])[]) {
         for (const o of options) {
             if (o[0] === k) return k;
         }