UNPKG

molstar

Version:

A comprehensive macromolecular library.

206 lines (205 loc) 10.3 kB
/** * Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> */ import { Bond, Structure, StructureElement } from '../../../mol-model/structure.js'; import { ColorThemeCategory } from '../../../mol-theme/color/categories.js'; import { deepEqual } from '../../../mol-util/index.js'; import { Color } from '../../../mol-util/color/index.js'; import { ColorNames } from '../../../mol-util/color/names.js'; import { ParamDefinition as PD } from '../../../mol-util/param-definition.js'; import { stringToWords } from '../../../mol-util/string.js'; import { isMVSStructure } from './is-mvs-model-prop.js'; import { ElementSet, SelectorParams, isSelectorAll, substructureFromSelector } from './selector.js'; /** Special value that can be used as color with null-like semantic (i.e. "no color provided"). * By some lucky coincidence, Mol* treats -1 as white. */ export const NoColor = Color(-1); /** Return true if `color` is a real color, false if it is `NoColor`. */ function isValidColor(color) { return color >= 0; } const DefaultBackgroundColor = ColorNames.white; /** Parameter definition for color theme "Multilayer" */ export function makeMultilayerColorThemeParams(colorThemeRegistry, ctx) { const colorThemeInfo = { help: (value) => { const { name, params } = value; const p = colorThemeRegistry.get(name); const ct = p.factory({}, params); return { description: ct.description, legend: ct.legend }; } }; const nestedThemeTypes = colorThemeRegistry.types.filter(([name, label, category]) => name !== MultilayerColorThemeName && colorThemeRegistry.get(name).isApplicable(ctx)); // Adding 'multilayer' theme itself would cause infinite recursion return { layers: PD.ObjectList({ theme: PD.Mapped('uniform', nestedThemeTypes, name => PD.Group(colorThemeRegistry.get(name).getParams({ structure: Structure.Empty })), colorThemeInfo), selection: SelectorParams, }, obj => stringToWords(obj.theme.name), { description: 'A list of layers, each defining a color theme. The last listed layer is the top layer (applies first). If the top layer does not provide color for a location or its selection does not cover the location, the underneath layers will apply.' }), background: PD.Color(DefaultBackgroundColor, { description: 'Color for elements where no layer applies' }), }; } /** Default values for `MultilayerColorThemeProps` */ export const DefaultMultilayerColorThemeProps = { layers: [], background: DefaultBackgroundColor }; /** Return color theme that assigns colors based on a list of nested color themes (layers). * The last layer in the list whose selection covers the given location * and which provides a valid (non-negative) color value will be used. * If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically * (the caller must ensure that any required custom properties be attached). */ function makeMultilayerColorTheme(ctx, props, colorThemeRegistry) { const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry); function structureElementColor(loc, isSecondary) { for (const layer of colorLayers) { const matches = !layer.elementSet || ElementSet.has(layer.elementSet, loc); if (!matches) continue; const color = layer.color(loc, isSecondary); if (!isValidColor(color)) continue; return color; } return props.background; } const auxLocation = StructureElement.Location.create(ctx.structure); const color = (location, isSecondary) => { if (StructureElement.Location.is(location)) { return structureElementColor(location, isSecondary); } else if (Bond.isLocation(location)) { // this will be applied for each bond twice, to get color of each half (a* refers to the adjacent atom, b* to the opposite atom) auxLocation.unit = location.aUnit; auxLocation.element = location.aUnit.elements[location.aIndex]; return structureElementColor(auxLocation, isSecondary); } return props.background; }; return { factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry), granularity, preferSmoothing, color: color, props: props, description: 'Combines colors from multiple color themes.', }; } const GRAN_INSTANCE = 1, GRAN_GROUP = 2, GRAN_VERTEX = 4; const granularityFlagsFromName = { 'uniform': 0, 'instance': GRAN_INSTANCE, 'group': GRAN_GROUP, 'groupInstance': GRAN_GROUP | GRAN_INSTANCE, 'vertex': GRAN_VERTEX, 'vertexInstance': GRAN_VERTEX | GRAN_INSTANCE, }; function granularityNameFromFlags(flags) { if (flags & GRAN_VERTEX) return flags & GRAN_INSTANCE ? 'vertexInstance' : 'vertex'; if (flags & GRAN_GROUP) return flags & GRAN_INSTANCE ? 'groupInstance' : 'group'; return flags & GRAN_INSTANCE ? 'instance' : 'uniform'; } function makeLayers(ctx, props, colorThemeRegistry) { var _a; const colorLayers = []; let granularityFlags = 0; let preferSmoothing = false; for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last const layer = props.layers[i]; const themeProvider = colorThemeRegistry.get(layer.theme.name); if (!themeProvider) { console.warn(`Skipping color theme '${layer.theme.name}', cannot find it in registry.`); continue; } if ((_a = themeProvider.ensureCustomProperties) === null || _a === void 0 ? void 0 : _a.attach) { console.warn(`Multilayer color theme: layer "${themeProvider.name}" has ensureCustomProperties.attach method, but Multilayer color theme does not call it. If the layer does not work, make sure you call ensureCustomProperties.attach somewhere.`); } const theme = themeProvider.factory(ctx, layer.theme.params); switch (theme.granularity) { case 'uniform': case 'instance': case 'group': case 'groupInstance': case 'vertex': case 'vertexInstance': let elementSet; let selectionGranularity; if (!ctx.structure) { elementSet = {}; selectionGranularity = 'uniform'; } else if (isSelectorAll(layer.selection)) { // Treating 'all' specially for performance reasons (it's expected to be used most often) elementSet = undefined; selectionGranularity = 'uniform'; } else { const substructure = substructureFromSelector(ctx.structure, layer.selection); elementSet = ElementSet.fromStructure(substructure); selectionGranularity = getSubstructureGranularity(ctx.structure, substructure); } colorLayers.push({ elementSet, color: theme.color }); granularityFlags |= granularityFlagsFromName[selectionGranularity]; granularityFlags |= granularityFlagsFromName[theme.granularity]; break; default: console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`); } if (theme.preferSmoothing) preferSmoothing = true; } return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing }; } function getSubstructureGranularity(parent, substructure) { var _a, _b, _c; const parentCounts = {}; for (const unit of parent.units) { const instance = unit.conformation.operator.instanceId; (_a = parentCounts[instance]) !== null && _a !== void 0 ? _a : (parentCounts[instance] = 0); parentCounts[instance] += unit.elements.length; } const childCounts = {}; const elementsPerInstance = {}; for (const unit of substructure.units) { const instance = unit.conformation.operator.instanceId; (_b = childCounts[instance]) !== null && _b !== void 0 ? _b : (childCounts[instance] = 0); childCounts[instance] += unit.elements.length; ((_c = elementsPerInstance[instance]) !== null && _c !== void 0 ? _c : (elementsPerInstance[instance] = {}))[unit.invariantId] = unit.elements; } const parentInstances = Object.keys(parentCounts); const childInstances = Object.keys(childCounts); const groupGranularity = !childInstances.every(inst => childCounts[inst] === parentCounts[inst]); let instanceGranularity; if (childInstances.length === 0) { instanceGranularity = false; } else if (childInstances.length < parentInstances.length) { instanceGranularity = true; } else { instanceGranularity = false; for (let i = 1; i < childInstances.length; i++) { if (!deepEqual(elementsPerInstance[childInstances[0]], elementsPerInstance[childInstances[i]])) { instanceGranularity = true; break; } } } if (groupGranularity) return instanceGranularity ? 'groupInstance' : 'group'; else return instanceGranularity ? 'instance' : 'uniform'; } /** Unique name for "Multilayer" color theme */ export const MultilayerColorThemeName = 'mvs-multilayer'; /** A thingy that is needed to register color theme "Multilayer" */ export function makeMultilayerColorThemeProvider(colorThemeRegistry) { return { name: MultilayerColorThemeName, label: 'MVS Multi-layer', category: ColorThemeCategory.Misc, factory: (ctx, props) => makeMultilayerColorTheme(ctx, props, colorThemeRegistry), getParams: (ctx) => makeMultilayerColorThemeParams(colorThemeRegistry, ctx), defaultValues: DefaultMultilayerColorThemeProps, isApplicable: (ctx) => !!ctx.structure && isMVSStructure(ctx.structure), }; }