ソースを参照

Merge pull request #838 from molstar/cartoon-color-nucleic

Cartoon improvements
Alexander Rose 1 年間 前
コミット
d26946e9ee

+ 2 - 0
CHANGELOG.md

@@ -15,6 +15,8 @@ Note that since we don't clearly distinguish between a public and private interf
 - Ensure consistent state for volume representation (#210)
 - Improve SSAO for thin geometry (e.g. lines)
 - Add snapshot support for structure selections
+- Add `nucleicProfile` parameter to cartoon representation
+- Add `cartoon` theme with separate colorings for for mainchain and sidechain visuals
 
 ## [v3.35.0] - 2023-05-14
 

+ 1 - 1
src/mol-geo/geometry/base.ts

@@ -93,7 +93,7 @@ export namespace BaseGeometry {
         if (!transform) transform = createIdentityTransform();
         const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
         const theme: Theme = {
-            color: UniformColorTheme({}, { value: colorValue }),
+            color: UniformColorTheme({}, { value: colorValue, lightness: 0, saturation: 0 }),
             size: UniformSizeTheme({}, { value: sizeValue })
         };
         return { transform, locationIterator, theme };

+ 11 - 7
src/mol-repr/structure/visual/polymer-trace-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -29,7 +29,8 @@ export const PolymerTraceMeshParams = {
     aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
     arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Size factor for sheet arrows' }),
     tubularHelices: PD.Boolean(false, { description: 'Draw alpha helices as tubes' }),
-    helixProfile: PD.Select('elliptical', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Protein and nucleic helix trace profile' }),
+    helixProfile: PD.Select('elliptical', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Protein helix trace profile' }),
+    nucleicProfile: PD.Select('square', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Nucleic strand trace profile' }),
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo)
@@ -43,7 +44,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
     const polymerElementCount = unit.polymerElements.length;
 
     if (!polymerElementCount) return Mesh.createEmpty(mesh);
-    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices, helixProfile } = props;
+    const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices, helixProfile, nucleicProfile } = props;
 
     const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2;
     const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
@@ -146,6 +147,8 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
                 for (let i = 0, il = normals.length; i < il; i++) normals[i] *= -1;
             }
 
+            const profile = isNucleicType ? nucleicProfile : helixProfile;
+
             if (radialSegments === 2) {
                 if (isNucleicType && !v.isCoarseBackbone) {
                     addRibbon(builderState, curvePoints, normals, binormals, segmentCount, heightValues, widthValues, 0);
@@ -156,10 +159,10 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
                 addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
             } else if (h1 === w1) {
                 addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
-            } else if (helixProfile === 'square') {
+            } else if (profile === 'square') {
                 addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
             } else {
-                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, helixProfile);
+                addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, profile);
             }
         }
 
@@ -184,7 +187,7 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
     return UnitsMeshVisual<PolymerTraceParams>({
         defaultProps: PD.getDefaultValues(PolymerTraceParams),
         createGeometry: createPolymerTraceMesh,
-        createLocationIterator: PolymerLocationIterator.fromGroup,
+        createLocationIterator: sg => PolymerLocationIterator.fromGroup(sg, true),
         getLoci: getPolymerElementLoci,
         eachLocation: eachPolymerElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTraceParams>, currentProps: PD.Values<PolymerTraceParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
@@ -196,7 +199,8 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
                 newProps.radialSegments !== currentProps.radialSegments ||
                 newProps.aspectRatio !== currentProps.aspectRatio ||
                 newProps.arrowFactor !== currentProps.arrowFactor ||
-                newProps.helixProfile !== currentProps.helixProfile
+                newProps.helixProfile !== currentProps.helixProfile ||
+                newProps.nucleicProfile !== currentProps.nucleicProfile
             );
 
             const secondaryStructureHash = SecondaryStructureProvider.get(newStructureGroup.structure).version;

+ 2 - 2
src/mol-repr/structure/visual/polymer-tube-mesh.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 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>
  */
@@ -117,7 +117,7 @@ export function PolymerTubeVisual(materialId: number): UnitsVisual<PolymerTubePa
     return UnitsMeshVisual<PolymerTubeParams>({
         defaultProps: PD.getDefaultValues(PolymerTubeParams),
         createGeometry: createPolymerTubeMesh,
-        createLocationIterator: PolymerLocationIterator.fromGroup,
+        createLocationIterator: sg => PolymerLocationIterator.fromGroup(sg, true),
         getLoci: getPolymerElementLoci,
         eachLocation: eachPolymerElement,
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<PolymerTubeParams>, currentProps: PD.Values<PolymerTubeParams>) => {

+ 6 - 3
src/mol-repr/structure/visual/util/polymer.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -41,7 +41,7 @@ export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
 }
 
 export namespace PolymerLocationIterator {
-    export function fromGroup(structureGroup: StructureGroup): LocationIterator {
+    export function fromGroup(structureGroup: StructureGroup, asSecondary = false): LocationIterator {
         const { group, structure } = structureGroup;
         const polymerElements = group.units[0].polymerElements;
         const groupCount = polymerElements.length;
@@ -53,7 +53,10 @@ export namespace PolymerLocationIterator {
             location.element = polymerElements[groupIndex];
             return location;
         };
-        return LocationIterator(groupCount, instanceCount, 1, getLocation);
+        function isSecondary(elementIndex: number, instanceIndex: number) {
+            return asSecondary;
+        }
+        return LocationIterator(groupCount, instanceCount, 1, getLocation, false, isSecondary);
     }
 }
 

+ 3 - 1
src/mol-theme/color.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -43,6 +43,7 @@ import { StructureIndexColorThemeProvider } from './color/structure-index';
 import { VolumeSegmentColorThemeProvider } from './color/volume-segment';
 import { ExternalVolumeColorThemeProvider } from './color/external-volume';
 import { ColorThemeCategory } from './color/categories';
+import { CartoonColorThemeProvider } from './color/cartoon';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
@@ -123,6 +124,7 @@ namespace ColorTheme {
     export const BuiltIn = {
         'atom-id': AtomIdColorThemeProvider,
         'carbohydrate-symbol': CarbohydrateSymbolColorThemeProvider,
+        'cartoon': CartoonColorThemeProvider,
         'chain-id': ChainIdColorThemeProvider,
         'element-index': ElementIndexColorThemeProvider,
         'element-symbol': ElementSymbolColorThemeProvider,

+ 111 - 0
src/mol-theme/color/cartoon.ts

@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color } from '../../mol-util/color';
+import { Location } from '../../mol-model/location';
+import type { ColorTheme } from '../color';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ThemeDataContext } from '../theme';
+import { ChainIdColorTheme, ChainIdColorThemeParams } from './chain-id';
+import { UniformColorTheme, UniformColorThemeParams } from './uniform';
+import { assertUnreachable } from '../../mol-util/type-helpers';
+import { EntityIdColorTheme, EntityIdColorThemeParams } from './entity-id';
+import { MoleculeTypeColorTheme, MoleculeTypeColorThemeParams } from './molecule-type';
+import { EntitySourceColorTheme, EntitySourceColorThemeParams } from './entity-source';
+import { ModelIndexColorTheme, ModelIndexColorThemeParams } from './model-index';
+import { StructureIndexColorTheme, StructureIndexColorThemeParams } from './structure-index';
+import { ColorThemeCategory } from './categories';
+import { ResidueNameColorTheme, ResidueNameColorThemeParams } from './residue-name';
+import { ScaleLegend, TableLegend } from '../../mol-util/legend';
+import { SecondaryStructureColorTheme, SecondaryStructureColorThemeParams } from './secondary-structure';
+import { ElementSymbolColorTheme, ElementSymbolColorThemeParams } from './element-symbol';
+
+const Description = 'Uses separate themes for coloring mainchain and sidechain visuals.';
+
+export const CartoonColorThemeParams = {
+    mainchain: PD.MappedStatic('molecule-type', {
+        uniform: PD.Group(UniformColorThemeParams),
+        'chain-id': PD.Group(ChainIdColorThemeParams),
+        'entity-id': PD.Group(EntityIdColorThemeParams),
+        'entity-source': PD.Group(EntitySourceColorThemeParams),
+        'molecule-type': PD.Group(MoleculeTypeColorThemeParams),
+        'model-index': PD.Group(ModelIndexColorThemeParams),
+        'structure-index': PD.Group(StructureIndexColorThemeParams),
+        'secondary-structure': PD.Group(SecondaryStructureColorThemeParams),
+    }),
+    sidechain: PD.MappedStatic('residue-name', {
+        uniform: PD.Group(UniformColorThemeParams),
+        'residue-name': PD.Group(ResidueNameColorThemeParams),
+        'element-symbol': PD.Group(ElementSymbolColorThemeParams),
+    }),
+};
+export type CartoonColorThemeParams = typeof CartoonColorThemeParams
+export function getCartoonColorThemeParams(ctx: ThemeDataContext) {
+    const params = PD.clone(CartoonColorThemeParams);
+    return params;
+}
+
+type CartoonColorThemeProps = PD.Values<CartoonColorThemeParams>
+
+function getMainchainTheme(ctx: ThemeDataContext, props: CartoonColorThemeProps['mainchain']) {
+    switch (props.name) {
+        case 'uniform': return UniformColorTheme(ctx, props.params);
+        case 'chain-id': return ChainIdColorTheme(ctx, props.params);
+        case 'entity-id': return EntityIdColorTheme(ctx, props.params);
+        case 'entity-source': return EntitySourceColorTheme(ctx, props.params);
+        case 'molecule-type': return MoleculeTypeColorTheme(ctx, props.params);
+        case 'model-index': return ModelIndexColorTheme(ctx, props.params);
+        case 'structure-index': return StructureIndexColorTheme(ctx, props.params);
+        case 'secondary-structure': return SecondaryStructureColorTheme(ctx, props.params);
+        default: assertUnreachable(props);
+    }
+}
+
+function getSidechainTheme(ctx: ThemeDataContext, props: CartoonColorThemeProps['sidechain']) {
+    switch (props.name) {
+        case 'uniform': return UniformColorTheme(ctx, props.params);
+        case 'residue-name': return ResidueNameColorTheme(ctx, props.params);
+        case 'element-symbol': return ElementSymbolColorTheme(ctx, props.params);
+        default: assertUnreachable(props);
+    }
+}
+
+export function CartoonColorTheme(ctx: ThemeDataContext, props: PD.Values<CartoonColorThemeParams>): ColorTheme<CartoonColorThemeParams> {
+    const mainchain = getMainchainTheme(ctx, props.mainchain);
+    const sidechain = getSidechainTheme(ctx, props.sidechain);
+
+    function color(location: Location, isSecondary: boolean): Color {
+        return isSecondary ? mainchain.color(location, false) : sidechain.color(location, false);
+    }
+
+    let legend: ScaleLegend | TableLegend | undefined = mainchain.legend;
+    if (mainchain.legend?.kind === 'table-legend' && sidechain.legend?.kind === 'table-legend') {
+        legend = {
+            kind: 'table-legend',
+            table: [...mainchain.legend.table, ...sidechain.legend.table]
+        };
+    }
+
+    return {
+        factory: CartoonColorTheme,
+        granularity: 'group',
+        preferSmoothing: false,
+        color,
+        props,
+        description: Description,
+        legend,
+    };
+}
+
+export const CartoonColorThemeProvider: ColorTheme.Provider<CartoonColorThemeParams, 'cartoon'> = {
+    name: 'cartoon',
+    label: 'Cartoon',
+    category: ColorThemeCategory.Misc,
+    factory: CartoonColorTheme,
+    getParams: getCartoonColorThemeParams,
+    defaultValues: PD.getDefaultValues(CartoonColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure
+};

+ 17 - 11
src/mol-theme/color/element-symbol.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -54,24 +54,30 @@ export function getElementSymbolColorThemeParams(ctx: ThemeDataContext) {
     return PD.clone(ElementSymbolColorThemeParams);
 }
 
+type ElementSymbolColorThemeProps = PD.Values<ElementSymbolColorThemeParams>
+
 export function elementSymbolColor(colorMap: ElementSymbolColors, element: ElementSymbol): Color {
     const c = colorMap[element as keyof ElementSymbolColors];
     return c === undefined ? DefaultElementSymbolColor : c;
 }
 
+function getCarbonTheme(ctx: ThemeDataContext, props: ElementSymbolColorThemeProps['carbonColor']) {
+    switch (props.name) {
+        case 'chain-id': return ChainIdColorTheme(ctx, props.params);
+        case 'entity-id': return EntityIdColorTheme(ctx, props.params);
+        case 'entity-source': return EntitySourceColorTheme(ctx, props.params);
+        case 'operator-name': return OperatorNameColorTheme(ctx, props.params);
+        case 'model-index': return ModelIndexColorTheme(ctx, props.params);
+        case 'structure-index': return StructureIndexColorTheme(ctx, props.params);
+        case 'element-symbol': return undefined;
+        default: assertUnreachable(props);
+    }
+}
+
 export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<ElementSymbolColorThemeParams>): ColorTheme<ElementSymbolColorThemeParams> {
     const colorMap = getAdjustedColorMap(props.colors.name === 'default' ? ElementSymbolColors : props.colors.params, props.saturation, props.lightness);
 
-    const pcc = props.carbonColor;
-    const carbonColor =
-        pcc.name === 'chain-id' ? ChainIdColorTheme(ctx, pcc.params).color :
-            pcc.name === 'entity-id' ? EntityIdColorTheme(ctx, pcc.params).color :
-                pcc.name === 'entity-source' ? EntitySourceColorTheme(ctx, pcc.params).color :
-                    pcc.name === 'operator-name' ? OperatorNameColorTheme(ctx, pcc.params).color :
-                        pcc.name === 'model-index' ? ModelIndexColorTheme(ctx, pcc.params).color :
-                            pcc.name === 'structure-index' ? StructureIndexColorTheme(ctx, pcc.params).color :
-                                pcc.name === 'element-symbol' ? undefined :
-                                    assertUnreachable(pcc);
+    const carbonColor = getCarbonTheme(ctx, props.carbonColor)?.color;
 
     function elementColor(element: ElementSymbol, location: Location) {
         return (carbonColor && element === 'C')

+ 17 - 10
src/mol-theme/color/illustrative.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2022 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>
  */
@@ -42,16 +42,23 @@ export function getIllustrativeColorThemeParams(ctx: ThemeDataContext) {
     return params;
 }
 
+type IllustrativeColorThemeProps = PD.Values<IllustrativeColorThemeParams>
+
+function getStyleTheme(ctx: ThemeDataContext, props: IllustrativeColorThemeProps['style']) {
+    switch (props.name) {
+        case 'uniform': return UniformColorTheme(ctx, props.params);
+        case 'chain-id': return ChainIdColorTheme(ctx, props.params);
+        case 'entity-id': return EntityIdColorTheme(ctx, props.params);
+        case 'entity-source': return EntitySourceColorTheme(ctx, props.params);
+        case 'molecule-type': return MoleculeTypeColorTheme(ctx, props.params);
+        case 'model-index': return ModelIndexColorTheme(ctx, props.params);
+        case 'structure-index': return StructureIndexColorTheme(ctx, props.params);
+        default: assertUnreachable(props);
+    }
+}
+
 export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<IllustrativeColorThemeParams>): ColorTheme<IllustrativeColorThemeParams> {
-    const { color: styleColor, legend } =
-        props.style.name === 'uniform' ? UniformColorTheme(ctx, props.style.params) :
-            props.style.name === 'chain-id' ? ChainIdColorTheme(ctx, props.style.params) :
-                props.style.name === 'entity-id' ? EntityIdColorTheme(ctx, props.style.params) :
-                    props.style.name === 'entity-source' ? EntitySourceColorTheme(ctx, props.style.params) :
-                        props.style.name === 'molecule-type' ? MoleculeTypeColorTheme(ctx, props.style.params) :
-                            props.style.name === 'model-index' ? ModelIndexColorTheme(ctx, props.style.params) :
-                                props.style.name === 'structure-index' ? StructureIndexColorTheme(ctx, props.style.params) :
-                                    assertUnreachable(props.style);
+    const { color: styleColor, legend } = getStyleTheme(ctx, props.style);
 
     function illustrativeColor(location: Location, typeSymbol: ElementSymbol) {
         const baseColor = styleColor(location, false);

+ 6 - 2
src/mol-theme/color/uniform.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -17,6 +17,8 @@ const Description = 'Gives everything the same, uniform color.';
 
 export const UniformColorThemeParams = {
     value: PD.Color(DefaultColor),
+    saturation: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
+    lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
 };
 export type UniformColorThemeParams = typeof UniformColorThemeParams
 export function getUniformColorThemeParams(ctx: ThemeDataContext) {
@@ -24,7 +26,9 @@ export function getUniformColorThemeParams(ctx: ThemeDataContext) {
 }
 
 export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<UniformColorThemeParams>): ColorTheme<UniformColorThemeParams> {
-    const color = defaults(props.value, DefaultColor);
+    let color = defaults(props.value, DefaultColor);
+    color = Color.saturate(color, props.saturation);
+    color = Color.lighten(color, props.lightness);
 
     return {
         factory: UniformColorTheme,