Browse Source

Merge branch 'master' of https://github.com/molstar/molstar

giagitom 1 year ago
parent
commit
36cf2853b2

+ 10 - 1
CHANGELOG.md

@@ -6,8 +6,17 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
-- Add `PluginContext.initialized` promise & support for it in the `Plugin` UI component.
 - Fix handling of PDB files with insertion codes (#945)
+- Fix de-/saturate of colors with no hue
+- Improve `distinctColors` function
+    - Add `sort` and `sampleCountFactor` parameters
+    - Fix clustering issues
+
+## [v3.41.0] - 2023-10-15
+
+- Add `PluginContext.initialized` promise & support for it in the `Plugin` UI component.
+- Fix undesired interaction between settings panel and the panel on the right.
+- Add ability to customize server parameters for `RCSBAssemblySymmetry`.
 
 ## [v3.40.1] - 2023-09-30
 

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "molstar",
-  "version": "3.40.1",
+  "version": "3.41.0",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "molstar",
-      "version": "3.40.1",
+      "version": "3.41.0",
       "license": "MIT",
       "dependencies": {
         "@types/argparse": "^2.0.11",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "3.40.1",
+  "version": "3.41.0",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {

+ 7 - 0
src/apps/viewer/app.ts

@@ -51,6 +51,7 @@ import { Backgrounds } from '../../extensions/backgrounds';
 import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
 import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
 import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
+import { RCSBAssemblySymmetryConfig } from '../../extensions/rcsb/assembly-symmetry/behavior';
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
@@ -114,6 +115,9 @@ const DefaultViewerOptions = {
     emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
     saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
     volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
+    rcsbAssemblySymmetryDefaultServerType: RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue,
+    rcsbAssemblySymmetryDefaultServerUrl: RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue,
+    rcsbAssemblySymmetryApplyColors: RCSBAssemblySymmetryConfig.ApplyColors.defaultValue,
 };
 type ViewerOptions = typeof DefaultViewerOptions;
 
@@ -191,6 +195,9 @@ export class Viewer {
                 [PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
                 [PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
                 [VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
+                [RCSBAssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
+                [RCSBAssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
+                [RCSBAssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
             ]
         };
 

+ 8 - 2
src/extensions/cellpack/color/generate.ts

@@ -45,8 +45,14 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
         const palette = getPalette(size, { palette: {
             name: 'generate',
             params: {
-                hue, chroma: [30, 80], luminance: [15, 85],
-                clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
+                hue,
+                chroma: [30, 80],
+                luminance: [15, 85],
+                clusteringStepCount: 50,
+                minSampleCount: 800,
+                maxCount: 75,
+                sampleCountFactor: 5,
+                sort: 'contrast'
             }
         } }, { minLabel: 'Min', maxLabel: 'Max' });
         legend = palette.legend;

+ 41 - 14
src/extensions/rcsb/assembly-symmetry/behavior.ts

@@ -5,12 +5,13 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider } from './prop';
+import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider, AssemblySymmetryDataParams } from './prop';
 import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
 import { AssemblySymmetryParams, AssemblySymmetryRepresentation } from './representation';
 import { AssemblySymmetryClusterColorThemeProvider } from './color';
 import { PluginStateTransform, PluginStateObject } from '../../../mol-plugin-state/objects';
 import { Task } from '../../../mol-task';
+import { PluginConfigItem } from '../../../mol-plugin/config';
 import { PluginContext } from '../../../mol-plugin/context';
 import { StateTransformer, StateAction, StateObject, StateTransform, StateObjectRef } from '../../../mol-state';
 import { GenericRepresentationRef } from '../../../mol-plugin-state/manager/structure/hierarchy-state';
@@ -77,14 +78,15 @@ export const InitAssemblySymmetry3D = StateAction.build({
         description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
     },
     from: PluginStateObject.Molecule.Structure,
-    isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
-})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
+    isApplicable: (a) => AssemblySymmetry.isApplicable(a.data),
+    params: (a, plugin: PluginContext) => getConfiguredDefaultParams(plugin)
+})(({ a, ref, state, params }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
     try {
         const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
-        await AssemblySymmetryDataProvider.attach(propCtx, a.data);
+        await AssemblySymmetryDataProvider.attach(propCtx, a.data, params);
         const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
         const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
-        await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
+        await AssemblySymmetryProvider.attach(propCtx, a.data, { ...params, symmetryIndex });
     } catch (e) {
         plugin.log.error(`Assembly Symmetry: ${e}`);
         return;
@@ -152,10 +154,6 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
 
 //
 
-export const AssemblySymmetryPresetParams = {
-    ...StructureRepresentationPresetProvider.CommonParams,
-};
-
 export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
     id: 'preset-structure-representation-rcsb-assembly-symmetry',
     display: {
@@ -165,7 +163,12 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
     isApplicable(a) {
         return AssemblySymmetry.isApplicable(a.data);
     },
-    params: () => AssemblySymmetryPresetParams,
+    params: (a, plugin) => {
+        return {
+            ...StructureRepresentationPresetProvider.CommonParams,
+            ...getConfiguredDefaultParams(plugin)
+        };
+    },
     async apply(ref, params, plugin) {
         const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
         const structure = structureCell?.obj?.data;
@@ -174,15 +177,16 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
         if (!AssemblySymmetryDataProvider.get(structure).value) {
             await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
                 const propCtx = { runtime, assetManager: plugin.managers.asset };
-                await AssemblySymmetryDataProvider.attach(propCtx, structure);
+                const propProps = { serverType: params.serverType, serverUrl: params.serverUrl };
+                await AssemblySymmetryDataProvider.attach(propCtx, structure, propProps);
                 const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
                 const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
-                await AssemblySymmetryProvider.attach(propCtx, structure, { symmetryIndex });
+                await AssemblySymmetryProvider.attach(propCtx, structure, { ...propProps, symmetryIndex });
             }));
         }
 
         const assemblySymmetry = await tryCreateAssemblySymmetry(plugin, structureCell);
-        const colorTheme = assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
+        const colorTheme = getRCSBAssemblySymmetryConfig(plugin).ApplyColors && assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
         const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
 
         return { components: preset.components, representations: { ...preset.representations, assemblySymmetry } };
@@ -194,4 +198,27 @@ export function tryCreateAssemblySymmetry(plugin: PluginContext, structure: Stat
     const assemblySymmetry = state.build().to(structure)
         .applyOrUpdateTagged(AssemblySymmetry.Tag.Representation, AssemblySymmetry3D, params, { state: initialState });
     return assemblySymmetry.commit({ revertOnError: true });
-}
+}
+
+//
+
+export const RCSBAssemblySymmetryConfig = {
+    DefaultServerType: new PluginConfigItem('rcsb-assembly-symmetry.server-type', AssemblySymmetryDataParams.serverType.defaultValue),
+    DefaultServerUrl: new PluginConfigItem('rcsb-assembly-symmetry.server-url', AssemblySymmetryDataParams.serverUrl.defaultValue),
+    ApplyColors: new PluginConfigItem('rcsb-assembly-symmetry.apply-colors', true),
+};
+
+export function getRCSBAssemblySymmetryConfig(plugin: PluginContext): { [key in keyof typeof RCSBAssemblySymmetryConfig]: NonNullable<typeof RCSBAssemblySymmetryConfig[key]['defaultValue']> } {
+    return {
+        ApplyColors: plugin.config.get(RCSBAssemblySymmetryConfig.ApplyColors) ?? RCSBAssemblySymmetryConfig.ApplyColors.defaultValue ?? true,
+        DefaultServerType: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerType) ?? RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue ?? AssemblySymmetryDataParams.serverType.defaultValue,
+        DefaultServerUrl: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerUrl) ?? RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue ?? AssemblySymmetryDataParams.serverUrl.defaultValue,
+    };
+}
+
+function getConfiguredDefaultParams(plugin: PluginContext) {
+    const config = getRCSBAssemblySymmetryConfig(plugin);
+    const params = PD.clone(AssemblySymmetryDataParams);
+    PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
+    return params;
+}

+ 38 - 3
src/extensions/rcsb/assembly-symmetry/prop.ts

@@ -20,6 +20,7 @@ import { SetUtils } from '../../../mol-util/set';
 import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
 import { compile } from '../../../mol-script/runtime/query/compiler';
 import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
+import { Asset } from '../../../mol-util/assets';
 
 const BiologicalAssemblyNames = new Set([
     'author_and_software_defined_assembly',
@@ -48,7 +49,7 @@ export namespace AssemblySymmetry {
         Representation = 'rcsb-assembly-symmetry-3d'
     }
 
-    export const DefaultServerUrl = 'https://data.rcsb.org/graphql';
+    export const DefaultServerUrl = 'https://data.rcsb.org/graphql'; // Alternative: 'https://www.ebi.ac.uk/pdbe/aggregated-api/pdb/symmetry' (if serverType is 'pdbe')
 
     export function isApplicable(structure?: Structure): boolean {
         return (
@@ -61,6 +62,8 @@ export namespace AssemblySymmetry {
     export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
         if (!isApplicable(structure)) return { value: [] };
 
+        if (props.serverType === 'pdbe') return fetch_PDBe(ctx, structure, props);
+
         const client = new GraphQLClient(props.serverUrl, ctx.assetManager);
         const variables: AssemblySymmetryQueryVariables = {
             assembly_id: structure.units[0].conformation.operator.assembly?.id || '',
@@ -77,6 +80,37 @@ export namespace AssemblySymmetry {
         return { value, assets: [result] };
     }
 
+    async function fetch_PDBe(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
+        const assembly_id = structure.units[0].conformation.operator.assembly?.id || '-1'; // should use '' instead of '-1' but the API does not support non-number assembly_id
+        const entry_id = structure.units[0].model.entryId.toLowerCase();
+        const url = `${props.serverUrl}/${entry_id}?assembly_id=${assembly_id}`;
+        const asset = Asset.getUrlAsset(ctx.assetManager, url);
+        let dataWrapper: Asset.Wrapper<'json'>;
+        try {
+            dataWrapper = await ctx.assetManager.resolve(asset, 'json').runInContext(ctx.runtime);
+        } catch (err) {
+            // PDBe API returns 404 when there are no symmetries -> treat as success with empty json in body
+            if (`${err}`.includes('404')) { // dirrrty
+                dataWrapper = Asset.Wrapper({}, asset, ctx.assetManager);
+            } else {
+                throw err;
+            }
+        }
+        const data = dataWrapper.data;
+
+        const value: AssemblySymmetryDataValue = (data[entry_id] ?? []).map((v: any) => ({
+            kind: 'Global Symmetry',
+            oligomeric_state: v.oligomeric_state,
+            stoichiometry: [v.stoichiometry],
+            symbol: v.symbol,
+            type: v.type,
+            clusters: [],
+            rotation_axes: v.rotation_axes,
+        }));
+
+        return { value, assets: [dataWrapper] };
+    }
+
     /** Returns the index of the first non C1 symmetry or -1 */
     export function firstNonC1(assemblySymmetryData: AssemblySymmetryDataValue) {
         for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
@@ -147,7 +181,8 @@ export function getSymmetrySelectParam(structure?: Structure) {
 //
 
 export const AssemblySymmetryDataParams = {
-    serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL' })
+    serverType: PD.Select('rcsb', [['rcsb', 'RCSB'], ['pdbe', 'PDBe']] as const),
+    serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL (if server type is RCSB) or PDBe API endpoint URL (if server type is PDBe)' })
 };
 export type AssemblySymmetryDataParams = typeof AssemblySymmetryDataParams
 export type AssemblySymmetryDataProps = PD.Values<AssemblySymmetryDataParams>
@@ -174,7 +209,7 @@ export const AssemblySymmetryDataProvider: CustomStructureProperty.Provider<Asse
 
 function getAssemblySymmetryParams(data?: Structure) {
     return {
-        ... AssemblySymmetryDataParams,
+        ...AssemblySymmetryDataParams,
         symmetryIndex: getSymmetrySelectParam(data)
     };
 }

+ 8 - 3
src/extensions/rcsb/assembly-symmetry/ui.tsx

@@ -6,7 +6,7 @@
 
 import { CollapsableState, CollapsableControls } from '../../../mol-plugin-ui/base';
 import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
-import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry } from './behavior';
+import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry, getRCSBAssemblySymmetryConfig } from './behavior';
 import { AssemblySymmetryProvider, AssemblySymmetryProps, AssemblySymmetryDataProvider, AssemblySymmetry } from './prop';
 import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -72,6 +72,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
     get params() {
         const structure = this.pivot.cell.obj?.data;
         const params = PD.clone(structure ? AssemblySymmetryProvider.getParams(structure) : AssemblySymmetryProvider.defaultParams);
+        params.serverType.isHidden = true;
         params.serverUrl.isHidden = true;
         return params;
     }
@@ -111,7 +112,9 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
                 }
             } else {
                 tryCreateAssemblySymmetry(this.plugin, s.cell);
-                await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
+                if (getRCSBAssemblySymmetryConfig(this.plugin).ApplyColors) {
+                    await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
+                }
             }
         }
     }
@@ -151,5 +154,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
 const EnableAssemblySymmetry3D = StateAction.build({
     from: PluginStateObject.Molecule.Structure,
 })(({ a, ref, state }, plugin: PluginContext) => Task.create('Enable Assembly Symmetry', async ctx => {
-    await AssemblySymmetryPreset.apply(ref, Object.create(null), plugin);
+    const presetParams = AssemblySymmetryPreset.params?.(a, plugin) as PD.Params | undefined;
+    const presetProps = presetParams ? PD.getDefaultValues(presetParams) : Object.create(null);
+    await AssemblySymmetryPreset.apply(ref, presetProps, plugin);
 }));

+ 13 - 7
src/mol-model-formats/structure/pdb/conect.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -20,14 +20,16 @@ export function parseConect(lines: Tokens, lineStart: number, lineEnd: number, s
     const conn_type_id: string[] = [];
 
     const ptnr1_label_asym_id: string[] = [];
-    const ptnr1_label_seq_id: number[] = [];
     const ptnr1_auth_seq_id: number[] = [];
     const ptnr1_label_atom_id: string[] = [];
+    const ptnr1_label_alt_id: string[] = [];
+    const ptnr1_PDB_ins_code: string[] = [];
 
     const ptnr2_label_asym_id: string[] = [];
-    const ptnr2_label_seq_id: number[] = [];
     const ptnr2_auth_seq_id: number[] = [];
     const ptnr2_label_atom_id: string[] = [];
+    const ptnr2_label_alt_id: string[] = [];
+    const ptnr2_PDB_ins_code: string[] = [];
 
     const pos = [11, 16, 21, 26];
 
@@ -58,13 +60,15 @@ export function parseConect(lines: Tokens, lineStart: number, lineEnd: number, s
 
             ptnr1_label_asym_id.push(sites.label_asym_id!.str(idxA));
             ptnr1_auth_seq_id.push(sites.auth_seq_id!.int(idxA));
-            ptnr1_label_seq_id.push(sites.label_seq_id!.int(idxA));
             ptnr1_label_atom_id.push(sites.label_atom_id!.str(idxA));
+            ptnr1_label_alt_id.push(sites.label_alt_id!.str(idxA));
+            ptnr1_PDB_ins_code.push(sites.pdbx_PDB_ins_code!.str(idxA));
 
             ptnr2_label_asym_id.push(sites.label_asym_id!.str(idxB));
             ptnr2_auth_seq_id.push(sites.auth_seq_id!.int(idxB));
-            ptnr2_label_seq_id.push(sites.label_seq_id!.int(idxB));
             ptnr2_label_atom_id.push(sites.label_atom_id!.str(idxB));
+            ptnr2_label_alt_id.push(sites.label_alt_id!.str(idxB));
+            ptnr2_PDB_ins_code.push(sites.pdbx_PDB_ins_code!.str(idxB));
 
             k += 1;
         }
@@ -76,13 +80,15 @@ export function parseConect(lines: Tokens, lineStart: number, lineEnd: number, s
 
         ptnr1_label_asym_id: CifField.ofStrings(ptnr1_label_asym_id),
         ptnr1_auth_seq_id: CifField.ofNumbers(ptnr1_auth_seq_id),
-        ptnr1_label_seq_id: CifField.ofNumbers(ptnr1_label_seq_id),
         ptnr1_label_atom_id: CifField.ofStrings(ptnr1_label_atom_id),
+        pdbx_ptnr1_label_alt_id: CifField.ofStrings(ptnr1_label_alt_id),
+        pdbx_ptnr1_PDB_ins_code: CifField.ofStrings(ptnr1_PDB_ins_code),
 
         ptnr2_label_asym_id: CifField.ofStrings(ptnr2_label_asym_id),
-        ptnr2_label_seq_id: CifField.ofNumbers(ptnr2_label_seq_id),
         ptnr2_auth_seq_id: CifField.ofNumbers(ptnr2_auth_seq_id),
         ptnr2_label_atom_id: CifField.ofStrings(ptnr2_label_atom_id),
+        pdbx_ptnr2_label_alt_id: CifField.ofStrings(ptnr2_label_alt_id),
+        pdbx_ptnr2_PDB_ins_code: CifField.ofStrings(ptnr2_PDB_ins_code),
     };
 
     return CifCategory.ofFields('struct_conn', struct_conn);

+ 1 - 3
src/mol-model-formats/structure/property/bonds/struct_conn.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2021 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2023 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -94,7 +94,6 @@ export namespace StructConn {
         const { conn_type_id, pdbx_dist_value, pdbx_value_order } = struct_conn;
         const p1 = {
             label_asym_id: struct_conn.ptnr1_label_asym_id,
-            label_seq_id: struct_conn.ptnr1_label_seq_id,
             auth_seq_id: struct_conn.ptnr1_auth_seq_id,
             label_atom_id: struct_conn.ptnr1_label_atom_id,
             label_alt_id: struct_conn.pdbx_ptnr1_label_alt_id,
@@ -103,7 +102,6 @@ export namespace StructConn {
         };
         const p2: typeof p1 = {
             label_asym_id: struct_conn.ptnr2_label_asym_id,
-            label_seq_id: struct_conn.ptnr2_label_seq_id,
             auth_seq_id: struct_conn.ptnr2_auth_seq_id,
             label_atom_id: struct_conn.ptnr2_label_atom_id,
             label_alt_id: struct_conn.pdbx_ptnr2_label_alt_id,

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

@@ -65,11 +65,11 @@ const DownloadStructure = StateAction.build({
                 }, { isFlat: true, label: 'PDB' }),
                 'pdb-dev': PD.Group({
                     provider: PD.Group({
-                        id: PD.Text('PDBDEV_00000001', { label: 'PDBDev Id(s)', description: 'One or more comma/space separated ids.' }),
+                        id: PD.Text('PDBDEV_00000001', { label: 'PDB-Dev Id(s)', description: 'One or more comma/space separated ids.' }),
                         encoding: PD.Select('bcif', PD.arrayToOptions(['cif', 'bcif'] as const)),
                     }, { pivot: 'id' }),
                     options
-                }, { isFlat: true, label: 'PDBDEV' }),
+                }, { isFlat: true, label: 'PDB-Dev' }),
                 'swissmodel': PD.Group({
                     id: PD.Text('Q9Y2I8', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
                     options

+ 1 - 1
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -100,7 +100,7 @@ const SimpleSettingsMapping = ParamMapping({
         if (r.top !== 'hidden' && (!c || c.top !== 'none')) layout.push('sequence');
         if (r.bottom !== 'hidden' && (!c || c.bottom !== 'none')) layout.push('log');
         if (r.left !== 'hidden' && (!c || c.left !== 'none')) layout.push('left');
-        if (r.right !== 'hidden' && (!c || c.left !== 'none')) layout.push('right');
+        if (r.right !== 'hidden' && (!c || c.right !== 'none')) layout.push('right');
         return { canvas: ctx.canvas3d?.props!, layout };
     }
 })({

+ 9 - 1
src/mol-util/color/color.ts

@@ -111,8 +111,16 @@ export namespace Color {
         return ((r << 16) | (g << 8) | b) as Color;
     }
 
+    export function hasHue(c: Color): boolean {
+        const r = c >> 16 & 255;
+        const g = c >> 8 & 255;
+        const b = c & 255;
+        return r !== g || r !== b;
+    }
+
     const tmpSaturateHcl = [0, 0, 0] as unknown as Hcl;
     export function saturate(c: Color, amount: number): Color {
+        if (!hasHue(c)) return c;
         Hcl.fromColor(tmpSaturateHcl, c);
         return Hcl.toColor(Hcl.saturate(tmpSaturateHcl, tmpSaturateHcl, amount));
     }
@@ -192,7 +200,7 @@ export interface ColorList {
     type: 'sequential' | 'diverging' | 'qualitative'
 }
 export function ColorList(label: string, type: 'sequential' | 'diverging' | 'qualitative', description: string, list: (number | [number, number])[]): ColorList {
-    return { label, description, list: list as Color[], type };
+    return { label, description, list: list as ColorListEntry[], type };
 }
 
 export type ColorTable<T extends { [k: string]: number[] }> = { [k in keyof T]: Color[] }

+ 46 - 34
src/mol-util/color/distinct.ts

@@ -15,26 +15,21 @@ import { deepEqual } from '../../mol-util';
 import { arraySum } from '../../mol-util/array';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ColorNames } from './names';
+import { Color } from './color';
 
 export const DistinctColorsParams = {
     hue: PD.Interval([1, 360], { min: 0, max: 360, step: 1 }),
     chroma: PD.Interval([40, 70], { min: 0, max: 100, step: 1 }),
     luminance: PD.Interval([15, 85], { min: 0, max: 100, step: 1 }),
+    sort: PD.Select('contrast', PD.arrayToOptions(['none', 'contrast'] as const), { description: 'no sorting leaves colors approximately ordered by hue' }),
 
     clusteringStepCount: PD.Numeric(50, { min: 10, max: 200, step: 1 }, { isHidden: true }),
-    minSampleCount: PD.Numeric(800, { min: 100, max: 5000, step: 100 }, { isHidden: true })
+    minSampleCount: PD.Numeric(800, { min: 100, max: 5000, step: 100 }, { isHidden: true }),
+    sampleCountFactor: PD.Numeric(5, { min: 1, max: 100, step: 1 }, { isHidden: true }),
 };
 export type DistinctColorsParams = typeof DistinctColorsParams
 export type DistinctColorsProps = PD.Values<typeof DistinctColorsParams>
 
-function distance(colorA: Lab, colorB: Lab) {
-    return Math.sqrt(
-        Math.pow(Math.abs(colorA[0] - colorB[0]), 2) +
-        Math.pow(Math.abs(colorA[1] - colorB[1]), 2) +
-        Math.pow(Math.abs(colorA[2] - colorB[2]), 2)
-    );
-}
-
 const LabTolerance = 2;
 const tmpCheckColorHcl = [0, 0, 0] as unknown as Hcl;
 const tmpCheckColorLab = [0, 0, 0] as unknown as Lab;
@@ -65,9 +60,9 @@ function sortByContrast(colors: Lab[]) {
     while (unsortedColors.length > 0) {
         const lastColor = sortedColors[sortedColors.length - 1];
         let nearest = 0;
-        let maxDist = Number.MIN_SAFE_INTEGER;
+        let maxDist = Number.NEGATIVE_INFINITY;
         for (let i = 0; i < unsortedColors.length; ++i) {
-            const dist = distance(lastColor, unsortedColors[i]);
+            const dist = Lab.distance(lastColor, unsortedColors[i]);
             if (dist > maxDist) {
                 maxDist = dist;
                 nearest = i;
@@ -78,18 +73,19 @@ function sortByContrast(colors: Lab[]) {
     return sortedColors;
 }
 
-function getSamples(count: number, p: DistinctColorsProps) {
-    const samples = new Map<string, Lab>();
-    const rangeDivider = Math.cbrt(count) * 1.001;
+function getSamples(count: number, p: DistinctColorsProps): Lab[] {
+    const samples = new Map<number, Lab>();
+    const rangeDivider = Math.ceil(Math.cbrt(count));
+    const hcl = Hcl();
 
     const hStep = Math.max((p.hue[1] - p.hue[0]) / rangeDivider, 1);
     const cStep = Math.max((p.chroma[1] - p.chroma[0]) / rangeDivider, 1);
     const lStep = Math.max((p.luminance[1] - p.luminance[0]) / rangeDivider, 1);
-    for (let h = p.hue[0]; h <= p.hue[1]; h += hStep) {
-        for (let c = p.chroma[0]; c <= p.chroma[1]; c += cStep) {
-            for (let l = p.luminance[0]; l <= p.luminance[1]; l += lStep) {
-                const lab = Lab.fromHcl(Lab(), Hcl.create(h, c, l));
-                if (checkColor(lab, p)) samples.set(lab.toString(), lab);
+    for (let h = p.hue[0] + hStep / 2; h <= p.hue[1]; h += hStep) {
+        for (let c = p.chroma[0] + cStep / 2; c <= p.chroma[1]; c += cStep) {
+            for (let l = p.luminance[0] + lStep / 2; l <= p.luminance[1]; l += lStep) {
+                const lab = Lab.fromHcl(Lab(), Hcl.set(hcl, h, c, l));
+                if (checkColor(lab, p)) samples.set(Lab.toColor(lab), lab);
             }
         }
     }
@@ -97,22 +93,36 @@ function getSamples(count: number, p: DistinctColorsProps) {
     return Array.from(samples.values());
 }
 
+function getClosestIndex(colors: Lab[], color: Lab) {
+    let minDist = Infinity;
+    let nearest = 0;
+
+    for (let j = 0; j < colors.length; j++) {
+        const dist = Lab.distance(color, colors[j]);
+        if (dist < minDist) {
+            minDist = dist;
+            nearest = j;
+        }
+    }
+
+    return nearest;
+}
+
 /**
  * Create a list of visually distinct colors
  */
-export function distinctColors(count: number, props: Partial<DistinctColorsProps> = {}) {
+export function distinctColors(count: number, props: Partial<DistinctColorsProps> = {}): Color[] {
     const p = { ...PD.getDefaultValues(DistinctColorsParams), ...props };
-
     if (count <= 0) return [];
 
-    const samples = getSamples(Math.max(p.minSampleCount, count * 5), p);
+    const samples = getSamples(Math.max(p.minSampleCount, count * p.sampleCountFactor), p);
     if (samples.length < count) {
         console.warn('Not enough samples to generate distinct colors, increase sample count.');
         return (new Array(count)).fill(ColorNames.lightgrey);
     }
 
     const colors: Lab[] = [];
-    const zonesProto: (Lab[])[] = [];
+    const zonesProto: Lab[][] = [];
     const sliceSize = Math.floor(samples.length / count);
 
     for (let i = 0; i < samples.length; i += sliceSize) {
@@ -123,18 +133,17 @@ export function distinctColors(count: number, props: Partial<DistinctColorsProps
 
     for (let step = 1; step <= p.clusteringStepCount; ++step) {
         const zones = deepClone(zonesProto);
+        const sampleList = deepClone(samples); // Immediately add the closest sample for each color
 
         // Find closest color for each sample
-        for (let i = 0; i < samples.length; ++i) {
-            let minDist = Number.MAX_SAFE_INTEGER;
-            let nearest = 0;
-            for (let j = 0; j < colors.length; j++) {
-                const dist = distance(samples[i], colors[j]);
-                if (dist < minDist) {
-                    minDist = dist;
-                    nearest = j;
-                }
-            }
+        for (let i = 0; i < colors.length; ++i) {
+            const idx = getClosestIndex(sampleList, colors[i]);
+            zones[i].push(samples[idx]);
+            sampleList.splice(idx, 1);
+        }
+
+        for (let i = 0; i < sampleList.length; ++i) {
+            const nearest = getClosestIndex(colors, samples[i]);
             zones[nearest].push(samples[i]);
         }
 
@@ -143,6 +152,8 @@ export function distinctColors(count: number, props: Partial<DistinctColorsProps
         for (let i = 0; i < zones.length; ++i) {
             const zone = zones[i];
             const size = zone.length;
+            if (size === 0) continue;
+
             const Ls: number[] = [];
             const As: number[] = [];
             const Bs: number[] = [];
@@ -163,5 +174,6 @@ export function distinctColors(count: number, props: Partial<DistinctColorsProps
         if (deepEqual(lastColors, colors)) break;
     }
 
-    return sortByContrast(colors).map(c => Lab.toColor(c));
+    const sorted = p.sort === 'contrast' ? sortByContrast(colors) : colors;
+    return sorted.map(c => Lab.toColor(c));
 }

+ 12 - 1
src/mol-util/color/spaces/hcl.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  *
@@ -43,6 +43,17 @@ namespace Hcl {
         return out;
     }
 
+    export function set(out: Hcl, h: number, c: number, l: number): Hcl {
+        out[0] = h;
+        out[1] = c;
+        out[2] = l;
+        return out;
+    }
+
+    export function hasHue(a: Hcl) {
+        return !isNaN(a[0]);
+    }
+
     const tmpFromColorLab = [0, 0, 0] as unknown as Lab;
     export function fromColor(out: Hcl, color: Color): Hcl {
         return Lab.toHcl(out, Lab.fromColor(tmpFromColorLab, color));

+ 16 - 1
src/mol-util/color/spaces/lab.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  *
@@ -44,6 +44,21 @@ namespace Lab {
         return out;
     }
 
+    export function set(out: Lab, l: number, a: number, b: number): Lab {
+        out[0] = l;
+        out[1] = a;
+        out[2] = b;
+        return out;
+    }
+
+    /** simple eucledian distance, not perceptually uniform */
+    export function distance(a: Lab, b: Lab) {
+        const x = b[0] - a[0],
+            y = b[1] - a[1],
+            z = b[2] - a[2];
+        return Math.sqrt(x * x + y * y + z * z);
+    }
+
     export function fromColor(out: Lab, color: Color): Lab {
         const [r, g, b] = Color.toRgb(color);
         const [x, y, z] = rgbToXyz(r, g, b);